diff --git a/packages/ui/client/components/views/ViewEditor.vue b/packages/ui/client/components/views/ViewEditor.vue index dfd3dbd8021e..963d00c865ad 100644 --- a/packages/ui/client/components/views/ViewEditor.vue +++ b/packages/ui/client/components/views/ViewEditor.vue @@ -79,12 +79,12 @@ watch([cm, failed], ([cmValue]) => { const e = i.result?.error const stacks = (e?.stacks || []).filter(i => i.file && i.file === props.file?.filepath) if (stacks.length) { - const pos = stacks[0].sourcePos || stacks[0] + const stack = stacks[0] const div = document.createElement('div') div.className = 'op80 flex gap-x-2 items-center' const pre = document.createElement('pre') pre.className = 'c-red-600 dark:c-red-400' - pre.textContent = `${' '.repeat(pos.column)}^ ${e?.nameStr}: ${e?.message}` + pre.textContent = `${' '.repeat(stack.column)}^ ${e?.nameStr}: ${e?.message}` div.appendChild(pre) const span = document.createElement('span') span.className = 'i-carbon-launch c-red-600 dark:c-red-400 hover:cursor-pointer min-w-1em min-h-1em' @@ -95,12 +95,12 @@ watch([cm, failed], ([cmValue]) => { placement: 'bottom', }, false) const el: EventListener = async () => { - await openInEditor(stacks[0].file, pos.line, pos.column) + await openInEditor(stacks[0].file, stack.line, stack.column) } div.appendChild(span) listeners.push([span, el, () => destroyTooltip(span)]) - handles.push(cm.value!.addLineClass(pos.line - 1, 'wrap', 'bg-red-500/10')) - widgets.push(cm.value!.addLineWidget(pos.line - 1, div)) + handles.push(cm.value!.addLineClass(stack.line - 1, 'wrap', 'bg-red-500/10')) + widgets.push(cm.value!.addLineWidget(stack.line - 1, div)) } }) if (!hasBeenEdited.value) diff --git a/packages/ui/client/components/views/ViewReport.cy.tsx b/packages/ui/client/components/views/ViewReport.cy.tsx index 4dbd790d9a90..7e87687ea8e5 100644 --- a/packages/ui/client/components/views/ViewReport.cy.tsx +++ b/packages/ui/client/components/views/ViewReport.cy.tsx @@ -9,10 +9,6 @@ const stackRowSelector = '[data-testid=stack]' const makeTextStack = () => ({ line: faker.datatype.number({ min: 0, max: 120 }), column: faker.datatype.number({ min: 0, max: 5000 }), - sourcePos: { - line: faker.datatype.number({ min: 121, max: 240 }), - column: faker.datatype.number({ min: 5001, max: 10000 }), - }, // Absolute file paths file: faker.system.filePath(), method: faker.hacker.verb(), @@ -49,9 +45,8 @@ describe('ViewReport', () => { cy.get(stackRowSelector).should('have.length', stacks.length) .get(stackRowSelector) .each(($stack, idx) => { - const { column, line, file: fileName, sourcePos } = stacks[idx] - expect($stack).not.to.contain.text(`${line}:${column}`) - expect($stack).to.contain.text(`${sourcePos.line}:${sourcePos.column}`) + const { column, line, file: fileName } = stacks[idx] + expect($stack).to.contain.text(`${line}:${column}`) expect($stack).to.contain.text(`- ${fileName}`) }) }) diff --git a/packages/ui/client/components/views/ViewReport.vue b/packages/ui/client/components/views/ViewReport.vue index 25d282c565c6..f7169ed37fc1 100644 --- a/packages/ui/client/components/views/ViewReport.vue +++ b/packages/ui/client/components/views/ViewReport.vue @@ -88,14 +88,6 @@ function relative(p: string) { return p } -function line(stack: ParsedStack) { - return stack.sourcePos?.line ?? stack.line -} - -function column(stack: ParsedStack) { - return stack.sourcePos?.column ?? stack.column -} - interface Diff { error: NonNullable> } type ResultWithDiff = Task['result'] & Diff function isDiffShowable(result?: Task['result']): result is ResultWithDiff { @@ -128,14 +120,14 @@ function diff(result: ResultWithDiff): string {
{{ task.result.error.name || task.result.error.nameStr }}: {{ task.result.error.message }}
-
 - {{ relative(stack.file) }}:{{ line(stack) }}:{{ column(stack) }}
+
 - {{ relative(stack.file) }}:{{ stack.line }}:{{ stack.column }}
diff --git a/packages/vitest/src/api/setup.ts b/packages/vitest/src/api/setup.ts
index d4c6408df886..a385164b6ce2 100644
--- a/packages/vitest/src/api/setup.ts
+++ b/packages/vitest/src/api/setup.ts
@@ -1,4 +1,5 @@
 import { promises as fs } from 'node:fs'
+
 import type { BirpcReturn } from 'birpc'
 import { createBirpc } from 'birpc'
 import { parse, stringify } from 'flatted'
@@ -8,6 +9,7 @@ import { API_PATH } from '../constants'
 import type { Vitest } from '../node'
 import type { File, ModuleGraphData, Reporter, TaskResultPack, UserConsoleLog } from '../types'
 import { getModuleGraph } from '../utils'
+import { parseStacktrace } from '../utils/source-map'
 import type { TransformResultWithSource, WebSocketEvents, WebSocketHandlers } from './types'
 
 export function setup(ctx: Vitest) {
@@ -121,6 +123,11 @@ class WebSocketReporter implements Reporter {
     if (this.clients.size === 0)
       return
 
+    packs.forEach(([, result]) => {
+      if (result?.error)
+        result.error.stacks = parseStacktrace(result.error)
+    })
+
     this.clients.forEach((client) => {
       client.onTaskUpdate?.(packs)
     })
diff --git a/packages/vitest/src/integrations/snapshot/port/inlineSnapshot.ts b/packages/vitest/src/integrations/snapshot/port/inlineSnapshot.ts
index 87f096233812..d62a00bf409b 100644
--- a/packages/vitest/src/integrations/snapshot/port/inlineSnapshot.ts
+++ b/packages/vitest/src/integrations/snapshot/port/inlineSnapshot.ts
@@ -1,6 +1,6 @@
 import { promises as fs } from 'fs'
 import type MagicString from 'magic-string'
-import { lineSplitRE, numberToPos, posToNumber } from '../../../utils/source-map'
+import { lineSplitRE, offsetToLineNumber, positionToOffset } from '../../../utils/source-map'
 import { getCallLastIndex } from '../../../utils'
 
 export interface InlineSnapshot {
@@ -21,7 +21,7 @@ export async function saveInlineSnapshots(
     const s = new MagicString(code)
 
     for (const snap of snaps) {
-      const index = posToNumber(code, snap)
+      const index = positionToOffset(code, snap.line, snap.column)
       replaceInlineSnap(code, s, index, snap.snapshot)
     }
 
@@ -50,8 +50,8 @@ function replaceObjectSnap(code: string, s: MagicString, index: number, newSnap:
 }
 
 function prepareSnapString(snap: string, source: string, index: number) {
-  const lineIndex = numberToPos(source, index).line
-  const line = source.split(lineSplitRE)[lineIndex - 1]
+  const lineNumber = offsetToLineNumber(source, index)
+  const line = source.split(lineSplitRE)[lineNumber - 1]
   const indent = line.match(/^\s*/)![0] || ''
   const indentNext = indent.includes('\t') ? `${indent}\t` : `${indent}  `
 
diff --git a/packages/vitest/src/node/error.ts b/packages/vitest/src/node/error.ts
index 5c33eabb3218..9bdd1532f725 100644
--- a/packages/vitest/src/node/error.ts
+++ b/packages/vitest/src/node/error.ts
@@ -1,10 +1,10 @@
 /* eslint-disable prefer-template */
 import { existsSync, readFileSync } from 'fs'
-import { join, normalize, relative } from 'pathe'
+import { normalize, relative } from 'pathe'
 import c from 'picocolors'
 import cliTruncate from 'cli-truncate'
-import type { ErrorWithDiff, ParsedStack, Position } from '../types'
-import { lineSplitRE, parseStacktrace, posToNumber } from '../utils/source-map'
+import type { ErrorWithDiff, ParsedStack } from '../types'
+import { lineSplitRE, parseStacktrace, positionToOffset } from '../utils/source-map'
 import { F_POINTER } from '../utils/figures'
 import { stringify } from '../integrations/chai/jest-matcher-utils'
 import { TypeCheckError } from '../typecheck/typechecker'
@@ -13,12 +13,6 @@ import type { Vitest } from './core'
 import { divider } from './reporters/renderers/utils'
 import type { Logger } from './logger'
 
-export function fileFromParsedStack(stack: ParsedStack) {
-  if (stack?.sourcePos?.source?.startsWith('..'))
-    return join(stack.file, '../', stack.sourcePos.source)
-  return stack.file
-}
-
 interface PrintErrorOptions {
   type?: string
   fullStack?: boolean
@@ -64,15 +58,10 @@ export async function printError(error: unknown, ctx: Vitest, options: PrintErro
     ctx.logger.error(c.yellow(e.frame))
   }
   else {
-    printStack(ctx, stacks, nearest, errorProperties, (s, pos) => {
+    printStack(ctx, stacks, nearest, errorProperties, (s) => {
       if (showCodeFrame && s === nearest && nearest) {
-        const file = fileFromParsedStack(nearest)
-        // could point to non-existing original file
-        // for example, when there is a source map file, but no source in node_modules
-        if (nearest.file === file || existsSync(file)) {
-          const sourceCode = readFileSync(file, 'utf-8')
-          ctx.logger.error(c.yellow(generateCodeFrame(sourceCode, 4, pos)))
-        }
+        const sourceCode = readFileSync(nearest.file, 'utf-8')
+        ctx.logger.error(c.yellow(generateCodeFrame(sourceCode, 4, s.line, s.column)))
       }
     })
   }
@@ -181,7 +170,7 @@ function printStack(
   stack: ParsedStack[],
   highlight: ParsedStack | undefined,
   errorProperties: Record,
-  onStack?: ((stack: ParsedStack, pos: Position) => void),
+  onStack?: ((stack: ParsedStack) => void),
 ) {
   if (!stack.length)
     return
@@ -189,13 +178,11 @@ function printStack(
   const logger = ctx.logger
 
   for (const frame of stack) {
-    const pos = frame.sourcePos || frame
     const color = frame === highlight ? c.yellow : c.gray
-    const file = fileFromParsedStack(frame)
-    const path = relative(ctx.config.root, file)
+    const path = relative(ctx.config.root, frame.file)
 
-    logger.error(color(` ${c.dim(F_POINTER)} ${[frame.method, c.dim(`${path}:${pos.line}:${pos.column}`)].filter(Boolean).join(' ')}`))
-    onStack?.(frame, pos)
+    logger.error(color(` ${c.dim(F_POINTER)} ${[frame.method, c.dim(`${path}:${frame.line}:${frame.column}`)].filter(Boolean).join(' ')}`))
+    onStack?.(frame)
 
     // reached at test file, skip the follow stack
     if (frame.file in ctx.state.filesMap)
@@ -213,12 +200,12 @@ function printStack(
 export function generateCodeFrame(
   source: string,
   indent = 0,
-  start: number | Position = 0,
-  end?: number,
+  lineNumber: number,
+  columnNumber: number,
   range = 2,
 ): string {
-  start = posToNumber(source, start)
-  end = end || start
+  const start = positionToOffset(source, lineNumber, columnNumber)
+  const end = start
   const lines = source.split(lineSplitRE)
   let count = 0
   let res: string[] = []
diff --git a/packages/vitest/src/node/reporters/json.ts b/packages/vitest/src/node/reporters/json.ts
index 986556597861..98e9b50f155f 100644
--- a/packages/vitest/src/node/reporters/json.ts
+++ b/packages/vitest/src/node/reporters/json.ts
@@ -188,7 +188,6 @@ export class JsonReporter implements Reporter {
     if (!frame)
       return
 
-    const pos = frame.sourcePos || frame
-    return { line: pos.line, column: pos.column }
+    return { line: frame.line, column: frame.column }
   }
 }
diff --git a/packages/vitest/src/node/reporters/junit.ts b/packages/vitest/src/node/reporters/junit.ts
index 666dcbce1ead..e66a7625dcc8 100644
--- a/packages/vitest/src/node/reporters/junit.ts
+++ b/packages/vitest/src/node/reporters/junit.ts
@@ -127,10 +127,9 @@ export class JUnitReporter implements Reporter {
 
     // TODO: This is same as printStack but without colors. Find a way to reuse code.
     for (const frame of stack) {
-      const pos = frame.sourcePos ?? frame
       const path = relative(this.ctx.config.root, frame.file)
 
-      await this.baseLog(` ${F_POINTER} ${[frame.method, `${path}:${pos.line}:${pos.column}`].filter(Boolean).join(' ')}`)
+      await this.baseLog(` ${F_POINTER} ${[frame.method, `${path}:${frame.line}:${frame.column}`].filter(Boolean).join(' ')}`)
 
       // reached at test file, skip the follow stack
       if (frame.file in this.ctx.state.filesMap)
diff --git a/packages/vitest/src/typecheck/typechecker.ts b/packages/vitest/src/typecheck/typechecker.ts
index a4f33b7cce4c..ce8ae34180b8 100644
--- a/packages/vitest/src/typecheck/typechecker.ts
+++ b/packages/vitest/src/typecheck/typechecker.ts
@@ -149,10 +149,6 @@ export class Typechecker {
             line: info.line,
             column: info.column,
             method: '',
-            sourcePos: {
-              line: info.line,
-              column: info.column,
-            },
           },
         ])
         Error.stackTraceLimit = limit
diff --git a/packages/vitest/src/types/general.ts b/packages/vitest/src/types/general.ts
index 3ddfd5ea07b6..f51be6877839 100644
--- a/packages/vitest/src/types/general.ts
+++ b/packages/vitest/src/types/general.ts
@@ -47,18 +47,11 @@ export interface UserConsoleLog {
   size: number
 }
 
-export interface Position {
-  source?: string
-  line: number
-  column: number
-}
-
 export interface ParsedStack {
   method: string
   file: string
   line: number
   column: number
-  sourcePos?: Position
 }
 
 export interface ErrorWithDiff extends Error {
diff --git a/packages/vitest/src/utils/source-map.ts b/packages/vitest/src/utils/source-map.ts
index e987e3d44ad1..081e2ecff103 100644
--- a/packages/vitest/src/utils/source-map.ts
+++ b/packages/vitest/src/utils/source-map.ts
@@ -1,5 +1,5 @@
 import { resolve } from 'pathe'
-import type { ErrorWithDiff, ParsedStack, Position } from '../types'
+import type { ErrorWithDiff, ParsedStack } from '../types'
 import { notNullish } from './base'
 
 export const lineSplitRE = /\r?\n/
@@ -91,31 +91,27 @@ export function parseStacktrace(e: ErrorWithDiff, full = false): ParsedStack[] {
   return stackFrames
 }
 
-export function posToNumber(
+export function positionToOffset(
   source: string,
-  pos: number | Position,
+  lineNumber: number,
+  columnNumber: number,
 ): number {
-  if (typeof pos === 'number')
-    return pos
   const lines = source.split(lineSplitRE)
-  const { line, column } = pos
   let start = 0
 
-  if (line > lines.length)
+  if (lineNumber > lines.length)
     return source.length
 
-  for (let i = 0; i < line - 1; i++)
+  for (let i = 0; i < lineNumber - 1; i++)
     start += lines[i].length + 1
 
-  return start + column
+  return start + columnNumber
 }
 
-export function numberToPos(
+export function offsetToLineNumber(
   source: string,
-  offset: number | Position,
-): Position {
-  if (typeof offset !== 'number')
-    return offset
+  offset: number,
+): number {
   if (offset > source.length) {
     throw new Error(
       `offset is longer than source length! offset ${offset} > length ${source.length}`,
@@ -124,14 +120,12 @@ export function numberToPos(
   const lines = source.split(lineSplitRE)
   let counted = 0
   let line = 0
-  let column = 0
   for (; line < lines.length; line++) {
     const lineLength = lines[line].length + 1
-    if (counted + lineLength >= offset) {
-      column = offset - counted + 1
+    if (counted + lineLength >= offset)
       break
-    }
+
     counted += lineLength
   }
-  return { line: line + 1, column }
+  return line + 1
 }