diff --git a/packages/vite-node/src/server.ts b/packages/vite-node/src/server.ts index b50b3cf049c8..fefe8e8bf805 100644 --- a/packages/vite-node/src/server.ts +++ b/packages/vite-node/src/server.ts @@ -192,7 +192,9 @@ export class ViteNodeServer { } protected async processTransformResult(result: TransformResult) { - return withInlineSourcemap(result) + return withInlineSourcemap(result, { + root: this.server.config.root, + }) } private async _transformRequest(id: string, customTransformMode?: 'web' | 'ssr') { diff --git a/packages/vite-node/src/source-map.ts b/packages/vite-node/src/source-map.ts index 581c197f0d6d..94e076f51bab 100644 --- a/packages/vite-node/src/source-map.ts +++ b/packages/vite-node/src/source-map.ts @@ -1,6 +1,7 @@ import type { TransformResult } from 'vite' import type { EncodedSourceMap } from '@jridgewell/trace-mapping' import { install } from './source-map-handler' +import { toFilePath } from './utils' interface InstallSourceMapSupportOptions { getSourceMap: (source: string) => EncodedSourceMap | null | undefined @@ -13,13 +14,24 @@ const VITE_NODE_SOURCEMAPPING_SOURCE = '//# sourceMappingSource=vite-node' const VITE_NODE_SOURCEMAPPING_URL = `${SOURCEMAPPING_URL}=data:application/json;charset=utf-8` const VITE_NODE_SOURCEMAPPING_REGEXP = new RegExp(`//# ${VITE_NODE_SOURCEMAPPING_URL};base64,(.+)`) -export function withInlineSourcemap(result: TransformResult) { +export function withInlineSourcemap(result: TransformResult, options: { + root: string // project root path of this resource +}) { const map = result.map let code = result.code if (!map || code.includes(VITE_NODE_SOURCEMAPPING_SOURCE)) return result + // sources path from `ViteDevServer` may be not a valid filesystem path (eg. /src/main.js), + // so we try to convert them to valid filesystem path + map.sources = map.sources.map((source) => { + if (!source) + return source + const { exists, path } = toFilePath(source, options.root) + return exists ? path : source + }) + // to reduce the payload size, we only inline vite node source map, because it's also the only one we use const OTHER_SOURCE_MAP_REGEXP = new RegExp(`//# ${SOURCEMAPPING_URL}=data:application/json[^,]+base64,(.+)`, 'g') while (OTHER_SOURCE_MAP_REGEXP.test(code)) diff --git a/test/stacktraces/fixtures/error-in-deps.test.js b/test/stacktraces/fixtures/error-in-deps.test.js new file mode 100644 index 000000000000..e02d9f1e19ba --- /dev/null +++ b/test/stacktraces/fixtures/error-in-deps.test.js @@ -0,0 +1,6 @@ +import { test } from 'vitest' +import { add } from './foo' + +test('error in deps', () => { + add() +}) diff --git a/test/stacktraces/fixtures/foo.js b/test/stacktraces/fixtures/foo.js new file mode 100644 index 000000000000..aeac594e5605 --- /dev/null +++ b/test/stacktraces/fixtures/foo.js @@ -0,0 +1,2 @@ +// eslint-disable-next-line no-undef +export const add = () => bar() diff --git a/test/stacktraces/test/__snapshots__/runner.test.ts.snap b/test/stacktraces/test/__snapshots__/runner.test.ts.snap index 646d97ab90b9..3fd11f080ae5 100644 --- a/test/stacktraces/test/__snapshots__/runner.test.ts.snap +++ b/test/stacktraces/test/__snapshots__/runner.test.ts.snap @@ -1,5 +1,21 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html +exports[`stacktrace should print error frame source file correctly > error-in-deps > error-in-deps 1`] = ` +"⎯⎯⎯⎯⎯⎯⎯ Failed Tests 1 ⎯⎯⎯⎯⎯⎯⎯ + + FAIL error-in-deps.test.js > error in deps +ReferenceError: bar is not defined + ❯ Module.add foo.js:2:26 + 1| // eslint-disable-next-line no-undef + 2| export const add = () => bar() + | ^ + 3| + ❯ error-in-deps.test.js:5:3 + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯ +" +`; + exports[`stacktraces should pick error frame if present > frame.spec.imba > frame.spec.imba 1`] = ` " FAIL frame.spec.imba [ frame.spec.imba ] imba-parser error: Unexpected 'CALL_END' @@ -44,6 +60,13 @@ exports[`stacktraces should respect sourcemaps > add-in-js.test.js > add-in-js.t " `; +exports[`stacktraces should respect sourcemaps > error-in-deps.test.js > error-in-deps.test.js 1`] = ` +" ❯ error-in-deps.test.js:5:3 + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯ +" +`; + exports[`stacktraces should respect sourcemaps > mocked-global.test.js > mocked-global.test.js 1`] = ` " ❯ mocked-global.test.js:6:13 4| diff --git a/test/stacktraces/test/runner.test.ts b/test/stacktraces/test/runner.test.ts index 4c9254afa4c1..78f647934a0d 100644 --- a/test/stacktraces/test/runner.test.ts +++ b/test/stacktraces/test/runner.test.ts @@ -62,3 +62,27 @@ describe('stacktraces should pick error frame if present', async () => { }, 30000) } }) + +describe('stacktrace should print error frame source file correctly', async () => { + const root = resolve(__dirname, '../fixtures') + const testFile = resolve(root, './error-in-deps.test.js') + it('error-in-deps', async () => { + // in Windows child_process is very unstable, we skip testing it + if (process.platform === 'win32' && process.env.CI) + return + + const { stderr } = await execa('npx', ['vitest', 'run', testFile], { + cwd: root, + reject: false, + stdio: 'pipe', + env: { + ...process.env, + CI: 'true', + NO_COLOR: 'true', + }, + }) + + // expect to print framestack of foo.js + expect(stderr).toMatchSnapshot('error-in-deps') + }, 30000) +}) diff --git a/test/vite-node/src/foo.js b/test/vite-node/src/foo.js new file mode 100644 index 000000000000..6e31ef4b12ed --- /dev/null +++ b/test/vite-node/src/foo.js @@ -0,0 +1 @@ +export const add = (a, b) => a + b diff --git a/test/vite-node/test/server.test.ts b/test/vite-node/test/server.test.ts index a7f165371110..26fb4e4509c9 100644 --- a/test/vite-node/test/server.test.ts +++ b/test/vite-node/test/server.test.ts @@ -1,5 +1,8 @@ +import { resolve } from 'pathe' import { ViteNodeServer } from 'vite-node/server' import { describe, expect, test, vi } from 'vitest' +import { createServer } from 'vite' +import { extractSourceMap } from '../../../packages/vite-node/src/source-map' describe('server works correctly', async () => { test('resolve id considers transform mode', async () => { @@ -26,4 +29,19 @@ describe('server works correctly', async () => { await vnServer.resolveId('/ssr', '/ssr path') expect(resolveId).toHaveBeenCalledWith('/ssr', '/ssr path', { ssr: true }) }) + test('fetchModule with id, and got sourcemap source in absolute path', async () => { + const server = await createServer({ + logLevel: 'error', + root: resolve(__dirname, '../'), + }) + const vnServer = new ViteNodeServer(server) + + // fetchModule in not a valid filesystem path + const fetchResult = await vnServer.fetchModule('/src/foo.js') + + const sourceMap = extractSourceMap(fetchResult.code!) + + // expect got sourcemap source in a valid filesystem path + expect(sourceMap?.sources[0]).toBe(resolve(__dirname, '../src/foo.js')) + }) })