From 0f1a73d77a532bbd5ccc4d90b1ccfa28dc6ea568 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Tue, 2 Dec 2025 19:29:14 +0900 Subject: [PATCH 1/5] fix: apply source maps for external module stack trace --- packages/vitest/package.json | 2 + packages/vitest/src/node/test-run.ts | 43 ++++++++++++++++++- pnpm-lock.yaml | 11 +++++ .../__snapshots__/stacktraces.test.ts.snap | 8 ++-- 4 files changed, 59 insertions(+), 5 deletions(-) diff --git a/packages/vitest/package.json b/packages/vitest/package.json index a841bff3997e..7e8da457a036 100644 --- a/packages/vitest/package.json +++ b/packages/vitest/package.json @@ -200,6 +200,7 @@ "@jridgewell/trace-mapping": "catalog:", "@opentelemetry/api": "^1.9.0", "@sinonjs/fake-timers": "14.0.0", + "@types/convert-source-map": "^2.0.3", "@types/estree": "catalog:", "@types/istanbul-lib-coverage": "catalog:", "@types/istanbul-reports": "catalog:", @@ -211,6 +212,7 @@ "acorn-walk": "catalog:", "birpc": "catalog:", "cac": "catalog:", + "convert-source-map": "^2.0.0", "empathic": "^2.0.0", "flatted": "catalog:", "happy-dom": "^20.0.11", diff --git a/packages/vitest/src/node/test-run.ts b/packages/vitest/src/node/test-run.ts index e9a3345407d7..50a00bc374cb 100644 --- a/packages/vitest/src/node/test-run.ts +++ b/packages/vitest/src/node/test-run.ts @@ -7,6 +7,7 @@ import type { } from '@vitest/runner' import type { TaskEventData, TestArtifact } from '@vitest/runner/types/tasks' import type { SerializedError } from '@vitest/utils' +import type { SourceMap } from 'rollup' import type { UserConsoleLog } from '../types/general' import type { Vitest } from './core' import type { TestProject } from './project' @@ -15,11 +16,14 @@ 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 path from 'node:path' import { isPrimitive } from '@vitest/utils/helpers' import { serializeValue } from '@vitest/utils/serialize' import { parseErrorStacktrace } from '@vitest/utils/source-map' + +import convertSourceMap from 'convert-source-map' import mime from 'mime/lite' import { basename, extname, resolve } from 'pathe' @@ -170,6 +174,18 @@ export class TestRun { else { error.stacks = parseErrorStacktrace(error, { frameFilter: project.config.onStackTrace, + getSourceMap(file) { + // This only handles external modules since + // source map is already applied for inlined modules. + // Module node exists due to Vitest fetch module, + // but transformResult should be empty for external modules. + const mod = project.vite.moduleGraph.getModuleById(file) + if (!mod?.transformResult && existsSync(file)) { + const code = readFileSync(file, 'utf-8') + const result = extractSourcemapFromFile(code, file) + return result + } + }, }) } }) @@ -291,3 +307,28 @@ function sanitizeFilePath(s: string): string { // eslint-disable-next-line no-control-regex return s.replace(/[\x00-\x2C\x2E\x2F\x3A-\x40\x5B-\x60\x7B-\x7F]+/g, '-') } + +// based on vite +// https://github.com/vitejs/vite/blob/84079a84ad94de4c1ef4f1bdb2ab448ff2c01196/packages/vite/src/node/server/sourcemap.ts#L149 +function extractSourcemapFromFile( + code: string, + filePath: string, +): SourceMap | undefined { + const map = ( + convertSourceMap.fromSource(code) + || (convertSourceMap.fromMapFileSource( + code, + createConvertSourceMapReadMap(filePath), + )) + )?.toObject() + return map +} + +function createConvertSourceMapReadMap(originalFileName: string) { + return (filename: string) => { + return readFileSync( + path.resolve(path.dirname(originalFileName), filename), + 'utf-8', + ) + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 150fde14b9c6..9fd8423bacfb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1008,6 +1008,9 @@ importers: '@sinonjs/fake-timers': specifier: 14.0.0 version: 14.0.0(patch_hash=6f7091fcf11165157b4bfb8dde6b5b6b7697e3efd6e9b53bb4bdef442f60a278) + '@types/convert-source-map': + specifier: ^2.0.3 + version: 2.0.3 '@types/estree': specifier: 'catalog:' version: 1.0.8 @@ -1041,6 +1044,9 @@ importers: cac: specifier: 'catalog:' version: 6.7.14(patch_hash=a8f0f3517a47ce716ed90c0cfe6ae382ab763b021a664ada2a608477d0621588) + convert-source-map: + specifier: ^2.0.0 + version: 2.0.0 empathic: specifier: ^2.0.0 version: 2.0.0 @@ -4192,6 +4198,9 @@ packages: '@types/codemirror@5.60.17': resolution: {integrity: sha512-AZq2FIsUHVMlp7VSe2hTfl5w4pcUkoFkM3zVsRKsn1ca8CXRDYvnin04+HP2REkwsxemuHqvDofdlhUWNpbwfw==} + '@types/convert-source-map@2.0.3': + resolution: {integrity: sha512-ag0BfJLZf6CQz8VIuRIEYQ5Ggwk/82uvTQf27RcpyDNbY0Vw49LIPqAxk5tqYfrCs9xDaIMvl4aj7ZopnYL8bA==} + '@types/d3-force@3.0.10': resolution: {integrity: sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==} @@ -12184,6 +12193,8 @@ snapshots: dependencies: '@types/tern': 0.23.4 + '@types/convert-source-map@2.0.3': {} + '@types/d3-force@3.0.10': {} '@types/d3-selection@3.0.11': {} diff --git a/test/cli/test/__snapshots__/stacktraces.test.ts.snap b/test/cli/test/__snapshots__/stacktraces.test.ts.snap index 2d47f18dcfc2..f99b3ed8127a 100644 --- a/test/cli/test/__snapshots__/stacktraces.test.ts.snap +++ b/test/cli/test/__snapshots__/stacktraces.test.ts.snap @@ -56,8 +56,8 @@ Error: __TEST_STACK_TS__ FAIL error-in-package.test.js > transpiled Error: __TEST_STACK_TRANSPILED__ - ❯ innerTestStack (NODE_MODULES)/@test/test-dep-error/transpiled.js:7:9 - ❯ testStack (NODE_MODULES)/@test/test-dep-error/transpiled.js:3:3 + ❯ innerTestStack (NODE_MODULES)/@test/test-dep-error/transpiled.js:22:8 + ❯ testStack (NODE_MODULES)/@test/test-dep-error/transpiled.js:12:2 ❯ error-in-package.test.js:16:22 14| 15| test('transpiled', () => { @@ -70,8 +70,8 @@ Error: __TEST_STACK_TRANSPILED__ FAIL error-in-package.test.js > transpiled inline Error: __TEST_STACK_TRANSPILED_INLINE__ - ❯ innerTestStack (NODE_MODULES)/@test/test-dep-error/transpiled-inline.js:7:9 - ❯ testStack (NODE_MODULES)/@test/test-dep-error/transpiled-inline.js:3:3 + ❯ innerTestStack (NODE_MODULES)/@test/test-dep-error/transpiled-inline.js:22:8 + ❯ testStack (NODE_MODULES)/@test/test-dep-error/transpiled-inline.js:12:2 ❯ error-in-package.test.js:20:28 18| 19| test('transpiled inline', () => { From 9db686011a9adb607bab4b8338026b712526301c Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Tue, 2 Dec 2025 19:30:36 +0900 Subject: [PATCH 2/5] cleanup --- packages/vitest/src/node/test-run.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/vitest/src/node/test-run.ts b/packages/vitest/src/node/test-run.ts index 50a00bc374cb..0cd69deb592f 100644 --- a/packages/vitest/src/node/test-run.ts +++ b/packages/vitest/src/node/test-run.ts @@ -22,7 +22,6 @@ import path from 'node:path' import { isPrimitive } from '@vitest/utils/helpers' import { serializeValue } from '@vitest/utils/serialize' import { parseErrorStacktrace } from '@vitest/utils/source-map' - import convertSourceMap from 'convert-source-map' import mime from 'mime/lite' import { basename, extname, resolve } from 'pathe' From c008c5662c7406169a281d0c95b90da2e5dcc543 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Wed, 3 Dec 2025 11:10:51 +0900 Subject: [PATCH 3/5] chore: build license --- packages/vitest/LICENSE.md | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/packages/vitest/LICENSE.md b/packages/vitest/LICENSE.md index ba4ea56c407a..36461d859ea0 100644 --- a/packages/vitest/LICENSE.md +++ b/packages/vitest/LICENSE.md @@ -280,6 +280,37 @@ Repository: egoist/cac --------------------------------------- +## convert-source-map +License: MIT +By: Thorsten Lorenz +Repository: git://github.com/thlorenz/convert-source-map.git + +> Copyright 2013 Thorsten Lorenz. +> All rights reserved. +> +> Permission is hereby granted, free of charge, to any person +> obtaining a copy of this software and associated documentation +> files (the "Software"), to deal in the Software without +> restriction, including without limitation the rights to use, +> copy, modify, merge, publish, distribute, sublicense, and/or sell +> copies of the Software, and to permit persons to whom the +> Software is furnished to do so, subject to the following +> conditions: +> +> The above copyright notice and this permission notice shall be +> included in all copies or substantial portions of the Software. +> +> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +> EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +> OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +> NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +> HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +> WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +> FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +> OTHER DEALINGS IN THE SOFTWARE. + +--------------------------------------- + ## empathic License: MIT By: Luke Edwards From 09c3955587ed37431209ec5cc62af2c01daac986 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Wed, 3 Dec 2025 11:25:56 +0900 Subject: [PATCH 4/5] fix: DecodedMap.resolvedSources --- packages/utils/src/source-map.ts | 2 +- test/cli/test/__snapshots__/stacktraces.test.ts.snap | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/utils/src/source-map.ts b/packages/utils/src/source-map.ts index f263b2caec12..f96c989085c5 100644 --- a/packages/utils/src/source-map.ts +++ b/packages/utils/src/source-map.ts @@ -354,7 +354,7 @@ export class DecodedMap { this._decodedMemo = memoizedState() this.url = from this.resolvedSources = (sources || []).map(s => - resolve(s || '', from), + resolve(from, '..', s || ''), ) } } diff --git a/test/cli/test/__snapshots__/stacktraces.test.ts.snap b/test/cli/test/__snapshots__/stacktraces.test.ts.snap index f99b3ed8127a..3f969715a8d9 100644 --- a/test/cli/test/__snapshots__/stacktraces.test.ts.snap +++ b/test/cli/test/__snapshots__/stacktraces.test.ts.snap @@ -56,8 +56,8 @@ Error: __TEST_STACK_TS__ FAIL error-in-package.test.js > transpiled Error: __TEST_STACK_TRANSPILED__ - ❯ innerTestStack (NODE_MODULES)/@test/test-dep-error/transpiled.js:22:8 - ❯ testStack (NODE_MODULES)/@test/test-dep-error/transpiled.js:12:2 + ❯ innerTestStack (NODE_MODULES)/@test/test-dep-error/transpiled.ts:22:8 + ❯ testStack (NODE_MODULES)/@test/test-dep-error/transpiled.ts:12:2 ❯ error-in-package.test.js:16:22 14| 15| test('transpiled', () => { @@ -70,8 +70,8 @@ Error: __TEST_STACK_TRANSPILED__ FAIL error-in-package.test.js > transpiled inline Error: __TEST_STACK_TRANSPILED_INLINE__ - ❯ innerTestStack (NODE_MODULES)/@test/test-dep-error/transpiled-inline.js:22:8 - ❯ testStack (NODE_MODULES)/@test/test-dep-error/transpiled-inline.js:12:2 + ❯ innerTestStack (NODE_MODULES)/@test/test-dep-error/transpiled-inline.ts:22:8 + ❯ testStack (NODE_MODULES)/@test/test-dep-error/transpiled-inline.ts:12:2 ❯ error-in-package.test.js:20:28 18| 19| test('transpiled inline', () => { From d77793c3787c747f7a1abf90f4ee04902feedeb6 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Thu, 5 Feb 2026 13:46:26 +0900 Subject: [PATCH 5/5] fix: handle covert-source-map exception --- packages/vitest/src/node/test-run.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/vitest/src/node/test-run.ts b/packages/vitest/src/node/test-run.ts index 387748e28ad1..cef2910b122d 100644 --- a/packages/vitest/src/node/test-run.ts +++ b/packages/vitest/src/node/test-run.ts @@ -332,9 +332,12 @@ function extractSourcemapFromFile( function createConvertSourceMapReadMap(originalFileName: string) { return (filename: string) => { - return readFileSync( - path.resolve(path.dirname(originalFileName), filename), - 'utf-8', - ) + // convertSourceMap can detect invalid filename from comments. + // fallback to empty source map to avoid errors. + const targetPath = path.resolve(path.dirname(originalFileName), filename) + if (existsSync(targetPath)) { + return readFileSync(targetPath, 'utf-8') + } + return '{}' } }