Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 2 additions & 18 deletions packages/browser/src/node/projectParent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import type {
import type { BrowserServerState } from './state'
import { readFileSync } from 'node:fs'
import { readFile } from 'node:fs/promises'
import { parseErrorStacktrace, parseStacktrace } from '@vitest/utils/source-map'
import { parseErrorStacktrace, parseStacktrace, retrieveSourceMapURL } from '@vitest/utils/source-map'
import { dirname, join, resolve } from 'pathe'
import { BrowserServerCDPHandler } from './cdp'
import builtinCommands from './commands/index'
Expand Down Expand Up @@ -64,7 +64,7 @@ export class ParentBrowserProject {
const result = this.vite.moduleGraph.getModuleById(id)?.transformResult
// this can happen for bundled dependencies in node_modules/.vite
if (result && !result.map) {
const sourceMapUrl = this.retrieveSourceMapURL(result.code)
const sourceMapUrl = retrieveSourceMapURL(result.code)
if (!sourceMapUrl) {
return null
}
Expand Down Expand Up @@ -262,20 +262,4 @@ export class ParentBrowserProject {
const decodedTestFile = decodeURIComponent(testFile)
return { sessionId, testFile: decodedTestFile }
}

private retrieveSourceMapURL(source: string): string | null {
const re
= /\/\/[@#]\s*sourceMappingURL=([^\s'"]+)\s*$|\/\*[@#]\s*sourceMappingURL=[^\s*'"]+\s*\*\/\s*$/gm
// Keep executing the search to find the *last* sourceMappingURL to avoid
// picking up sourceMappingURLs from comments, strings, etc.
let lastMatch, match
// eslint-disable-next-line no-cond-assign
while ((match = re.exec(source))) {
lastMatch = match
}
if (!lastMatch) {
return null
}
return lastMatch[1]
}
}
24 changes: 21 additions & 3 deletions packages/browser/src/node/rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@
import type { WebSocketBrowserEvents, WebSocketBrowserHandlers } from '../types'
import type { ParentBrowserProject } from './projectParent'
import type { BrowserServerState } from './state'
import { existsSync, promises as fs } from 'node:fs'
import { existsSync, promises as fs, readFileSync } from 'node:fs'
import { AutomockedModule, AutospiedModule, ManualMockedModule, RedirectedModule } from '@vitest/mocker'
import { ServerMockResolver } from '@vitest/mocker/node'
import { retrieveSourceMapURL } from '@vitest/utils/source-map'
import { createBirpc } from 'birpc'
import { parse, stringify } from 'flatted'
import { dirname, join } from 'pathe'
import { dirname, join, resolve } from 'pathe'
import { createDebugger, isFileServingAllowed, isValidApiRequest } from 'vitest/node'
import { WebSocketServer } from 'ws'

Expand Down Expand Up @@ -204,7 +205,24 @@
},
getBrowserFileSourceMap(id) {
const mod = globalServer.vite.moduleGraph.getModuleById(id)
return mod?.transformResult?.map
const result = mod?.transformResult
// this can happen for bundled dependencies in node_modules/.vite
if (result && !result.map) {
const sourceMapUrl = retrieveSourceMapURL(result.code)
if (!sourceMapUrl) {
return null
}
const filepathDir = dirname(id)
const sourceMapPath = resolve(filepathDir, sourceMapUrl)
try {
const map = JSON.parse(readFileSync(sourceMapPath, 'utf-8'))
return map
}
catch {
return null
}
}
return result?.map
},
cancelCurrentRun(reason) {
vitest.cancelCurrentRun(reason)
Expand Down Expand Up @@ -354,7 +372,7 @@
}
}

// Serialization support utils.

Check failure on line 375 in packages/browser/src/node/rpc.ts

View workflow job for this annotation

GitHub Actions / Lint: node-latest, ubuntu-latest

'retrieveSourceMapURL' is already defined
function cloneByOwnProperties(value: any) {
// Clones the value's properties into a new Object. The simpler approach of
// Object.assign() won't work in the case that properties are not enumerable.
Expand Down
14 changes: 14 additions & 0 deletions packages/utils/src/source-map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -383,3 +383,17 @@ export function getOriginalPosition(
}
return result
}

export function retrieveSourceMapURL(source: string): string | null {
const re = /\/\/[@#]\s*sourceMappingURL=([^\s'"]+)\s*$|\/\*[@#]\s*sourceMappingURL=[^\s*'"]+\s*\*\/\s*$/gm
// continue executing the search to find the *last* sourceMappingURL to avoid picking up souceMappingURL from comments, strings, etc
let lastMatch, match
// eslint-disable-next-line no-cond-assign
while ((match = re.exec(source))) {
lastMatch = match
}
if (!lastMatch) {
return null
}
return lastMatch[1]
}
26 changes: 23 additions & 3 deletions packages/vitest/src/node/test-run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,13 @@ import type { TestSpecification } from './spec'
import type { TestRunEndReason } from './types/reporter'
import assert from 'node:assert'
import { createHash } from 'node:crypto'
import { existsSync } from 'node:fs'
import { existsSync, readFileSync } from 'node:fs'
import { copyFile, mkdir, writeFile } from 'node:fs/promises'
import { isPrimitive } from '@vitest/utils/helpers'
import { serializeValue } from '@vitest/utils/serialize'
import { parseErrorStacktrace } from '@vitest/utils/source-map'
import { parseErrorStacktrace, retrieveSourceMapURL } from '@vitest/utils/source-map'
import mime from 'mime/lite'
import { basename, extname, resolve } from 'pathe'
import { basename, dirname, extname, resolve } from 'pathe'

export class TestRun {
constructor(private vitest: Vitest) {}
Expand Down Expand Up @@ -155,6 +155,26 @@ export class TestRun {
else {
error.stacks = parseErrorStacktrace(error, {
frameFilter: project.config.onStackTrace,
getSourceMap: (id) => {
const result = project.vite.moduleGraph.getModuleById(id)?.transformResult
// this could happen for bundled dependencies in node_modules/.vite
if (result && !result.map) {
const sourceMapUrl = retrieveSourceMapURL(result.code)
if (!sourceMapUrl) {
return null
}
const filepathDir = dirname(id)
const sourceMapPath = resolve(filepathDir, sourceMapUrl)
try {
const map = JSON.parse(readFileSync(sourceMapPath, 'utf-8'))
return map
}
catch {
return null
}
}
return result?.map
},
})
}
})
Expand Down
150 changes: 150 additions & 0 deletions test/core/test/external-sourcemap.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import { expect, test } from 'vitest'
import { runInlineTests } from '../../test-utils'

test('should load external source maps for bundled dependencies', async () => {
const { stderr } = await runInlineTests(
{
'bundled-dep.js': `
// Simulated bundled code (transformed)
function __bundled__throwError() {
throw new Error('Error from bundled dependency');
}
export function callError() {
__bundled__throwError();
}
//# sourceMappingURL=bundled-dep.js.map
`,
'bundled-dep.js.map': JSON.stringify({
version: 3,
file: 'bundled-dep.js',
sources: ['original-source.ts'],
sourcesContent: [
`// Original source before bundling
function throwError() {
throw new Error('Error from bundled dependency');
}
export function callError() {
throwError();
}
`,
],
names: [],
mappings: ';;AACA,SAAS,UAAU,GAAG;AACpB,QAAM,IAAI,KAAK,CAAC,gCAAgC,CAAC;AACnD;AACA,OAAO,SAAS,SAAS,GAAG;AAC1B,YAAU,EAAE;AACd',
}),
'test.spec.ts': `
import { test, expect } from 'vitest';
import { callError } from './bundled-dep.js';

test('error with external source map', () => {
try {
callError();
expect.fail('Should have thrown an error');
} catch (error) {
// The error should reference the original source file
expect(error.message).toBe('Error from bundled dependency');
// Stack trace should be parsed with source maps
expect(error.stack).toBeTruthy();
}
});
`,
},
{
pool: 'threads',
},
)

// The test should pass, meaning source maps were loaded
expect(stderr).not.toContain('FAIL')
})

test('should handle missing external source maps gracefully', async () => {
const { stderr } = await runInlineTests(
{
'bundled-dep-no-map.js': `
// Bundled code with source map reference but no actual map file
function __bundled__throwError() {
throw new Error('Error without source map');
}
export function callError() {
__bundled__throwError();
}
//# sourceMappingURL=non-existent.js.map
`,
'test.spec.ts': `
import { test, expect } from 'vitest';
import { callError } from './bundled-dep-no-map.js';

test('error without external source map', () => {
try {
callError();
expect.fail('Should have thrown an error');
} catch (error) {
// Should still work even without source map
expect(error.message).toBe('Error without source map');
expect(error.stack).toBeTruthy();
}
});
`,
},
{
pool: 'threads',
},
)

// The test should still pass even without source map
expect(stderr).not.toContain('FAIL')
})

test('should parse stack traces with external source maps in error output', async () => {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you explain how this test case is testing external library's source map? It looks like it's importing as user's file via import './bundled-lib.js'.

const result = await runInlineTests(
{
'bundled-lib.js': `
// Simulated bundled library code
export function deepFunction() {
throw new Error('Deep error in bundled code');
}
export function middleFunction() {
deepFunction();
}
export function topFunction() {
middleFunction();
}
//# sourceMappingURL=bundled-lib.js.map
`,
'bundled-lib.js.map': JSON.stringify({
version: 3,
file: 'bundled-lib.js',
sources: ['src/lib.ts'],
sourcesContent: [
`// Original library source
export function deepFunction() {
throw new Error('Deep error in bundled code');
}
export function middleFunction() {
deepFunction();
}
export function topFunction() {
middleFunction();
}
`,
],
names: [],
mappings: ';;AACA,OAAO,SAAS,YAAY,GAAG;AAC7B,QAAM,IAAI,KAAK,CAAC,4BAA4B,CAAC;AAC/C;AACA,OAAO,SAAS,cAAc,GAAG;AAC/B,cAAY,EAAE;AAChB;AACA,OAAO,SAAS,WAAW,GAAG;AAC5B,gBAAc,EAAE;AAClB',
}),
'test.spec.ts': `
import { test, expect } from 'vitest';
import { topFunction } from './bundled-lib.js';

test('should show original source in stack trace', () => {
expect(() => topFunction()).toThrow('Deep error in bundled code');
});
`,
},
{
pool: 'threads',
},
)

// The test itself should pass - this verifies source maps work
expect(result.stderr).not.toContain('FAIL')
})
Loading