diff --git a/packages/utils/src/source-map.ts b/packages/utils/src/source-map.ts index efc9a9e322a4..ac28116fa1f6 100644 --- a/packages/utils/src/source-map.ts +++ b/packages/utils/src/source-map.ts @@ -364,7 +364,7 @@ export class DecodedMap { this._decodedMemo = memoizedState() this.url = from this.resolvedSources = (sources || []).map(s => - resolve(s || '', from), + resolve(from, '..', s || ''), ) } } diff --git a/packages/vitest/LICENSE.md b/packages/vitest/LICENSE.md index d2883c9b3d11..89e5dd9cfbcd 100644 --- a/packages/vitest/LICENSE.md +++ b/packages/vitest/LICENSE.md @@ -316,6 +316,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 diff --git a/packages/vitest/package.json b/packages/vitest/package.json index d19611f7f5f8..c906043cbb01 100644 --- a/packages/vitest/package.json +++ b/packages/vitest/package.json @@ -198,6 +198,7 @@ "@jridgewell/trace-mapping": "catalog:", "@opentelemetry/api": "^1.9.0", "@sinonjs/fake-timers": "15.0.0", + "@types/convert-source-map": "^2.0.3", "@types/estree": "catalog:", "@types/istanbul-lib-coverage": "catalog:", "@types/istanbul-reports": "catalog:", @@ -210,6 +211,7 @@ "acorn-walk": "catalog:", "birpc": "catalog:", "cac": "catalog:", + "convert-source-map": "^2.0.0", "empathic": "^2.0.0", "flatted": "catalog:", "happy-dom": "^20.4.0", diff --git a/packages/vitest/src/node/test-run.ts b/packages/vitest/src/node/test-run.ts index 818e9d130fba..cef2910b122d 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,13 @@ import type { TestSpecification } from './test-specification' 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 +173,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 + } + }, }) } }) @@ -298,3 +313,31 @@ 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) => { + // 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 '{}' + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b515eabc0c7e..7d7bf3ca84ba 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1066,6 +1066,9 @@ importers: '@sinonjs/fake-timers': specifier: 15.0.0 version: 15.0.0(patch_hash=8f3309cba0158608885141fb640e96b064570f7399136966ff13523bdaf678b2) + '@types/convert-source-map': + specifier: ^2.0.3 + version: 2.0.3 '@types/estree': specifier: 'catalog:' version: 1.0.8 @@ -1102,6 +1105,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 @@ -4755,6 +4761,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==} @@ -12821,6 +12830,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..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:7:9 - ❯ testStack (NODE_MODULES)/@test/test-dep-error/transpiled.js:3:3 + ❯ 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:7:9 - ❯ testStack (NODE_MODULES)/@test/test-dep-error/transpiled-inline.js:3:3 + ❯ 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', () => {