Skip to content

Commit 8c969de

Browse files
authored
feat: add syntax highlighting to error messages (#4813)
1 parent 96dc6e9 commit 8c969de

File tree

13 files changed

+130
-26
lines changed

13 files changed

+130
-26
lines changed

examples/mocks/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,11 @@
1818
"tinyspy": "^0.3.2"
1919
},
2020
"devDependencies": {
21-
"@vitest/ui": "latest",
21+
"@vitest/ui": "workspace:*",
2222
"react": "^18.0.0",
2323
"sweetalert2": "^11.6.16",
2424
"vite": "latest",
25-
"vitest": "latest",
25+
"vitest": "workspace:*",
2626
"vue": "^3.3.8",
2727
"zustand": "^4.1.1"
2828
},

packages/utils/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969
},
7070
"devDependencies": {
7171
"@jridgewell/trace-mapping": "^0.3.20",
72-
"@types/estree": "^1.0.5"
72+
"@types/estree": "^1.0.5",
73+
"tinyhighlight": "^0.3.2"
7374
}
7475
}

packages/utils/rollup.config.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import esbuild from 'rollup-plugin-esbuild'
44
import dts from 'rollup-plugin-dts'
55
import resolve from '@rollup/plugin-node-resolve'
66
import json from '@rollup/plugin-json'
7+
import commonjs from '@rollup/plugin-commonjs'
78

89
const require = createRequire(import.meta.url)
910
const pkg = require('./package.json')
@@ -32,6 +33,7 @@ const plugins = [
3233
esbuild({
3334
target: 'node14',
3435
}),
36+
commonjs(),
3537
]
3638

3739
export default defineConfig([

packages/utils/src/colors.ts

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,16 +27,17 @@ const colorsMap = {
2727
bgWhite: ['\x1B[47m', '\x1B[49m'],
2828
} as const
2929

30-
type ColorName = keyof typeof colorsMap
31-
type ColorsMethods = {
32-
[Key in ColorName]: {
33-
(input: unknown): string
34-
open: string
35-
close: string
36-
}
30+
export type ColorName = keyof typeof colorsMap
31+
export interface ColorMethod {
32+
(input: unknown): string
33+
open: string
34+
close: string
35+
}
36+
export type ColorsMethods = {
37+
[Key in ColorName]: ColorMethod
3738
}
3839

39-
type Colors = ColorsMethods & {
40+
export type Colors = ColorsMethods & {
4041
isColorSupported: boolean
4142
reset: (input: unknown) => string
4243
}

packages/utils/src/highlight.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { type TokenColors, highlight as baseHighlight } from 'tinyhighlight'
2+
import type { ColorName } from './colors'
3+
import { getColors } from './colors'
4+
5+
type Colors = Record<ColorName, (input: string) => string>
6+
7+
function getDefs(c: Colors): TokenColors {
8+
const Invalid = (text: string) => c.white(c.bgRed(c.bold(text)))
9+
return {
10+
Keyword: c.magenta,
11+
IdentifierCapitalized: c.yellow,
12+
Punctuator: c.yellow,
13+
StringLiteral: c.green,
14+
NoSubstitutionTemplate: c.green,
15+
MultiLineComment: c.gray,
16+
SingleLineComment: c.gray,
17+
RegularExpressionLiteral: c.cyan,
18+
NumericLiteral: c.blue,
19+
TemplateHead: text => c.green(text.slice(0, text.length - 2)) + c.cyan(text.slice(-2)),
20+
TemplateTail: text => c.cyan(text.slice(0, 1)) + c.green(text.slice(1)),
21+
TemplateMiddle: text => c.cyan(text.slice(0, 1)) + c.green(text.slice(1, text.length - 2)) + c.cyan(text.slice(-2)),
22+
IdentifierCallable: c.blue,
23+
PrivateIdentifierCallable: text => `#${c.blue(text.slice(1))}`,
24+
Invalid,
25+
26+
JSXString: c.green,
27+
JSXIdentifier: c.yellow,
28+
JSXInvalid: Invalid,
29+
JSXPunctuator: c.yellow,
30+
}
31+
}
32+
33+
interface HighlightOptions {
34+
jsx?: boolean
35+
colors?: Colors
36+
}
37+
38+
export function highlight(code: string, options: HighlightOptions = { jsx: false }) {
39+
return baseHighlight(code, {
40+
jsx: options.jsx,
41+
colors: getDefs(options.colors || getColors()),
42+
})
43+
}

packages/utils/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ export * from './constants'
88
export * from './colors'
99
export * from './base'
1010
export * from './offset'
11+
export * from './highlight'

packages/utils/src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,4 +44,5 @@ export interface ErrorWithDiff extends Error {
4444
type?: string
4545
frame?: string
4646
diff?: string
47+
codeFrame?: string
4748
}

packages/vitest/src/node/core.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -670,13 +670,15 @@ export class Vitest {
670670

671671
const onChange = (id: string) => {
672672
id = slash(id)
673+
this.logger.clearHighlightCache(id)
673674
updateLastChanged(id)
674675
const needsRerun = this.handleFileChanged(id)
675676
if (needsRerun.length)
676677
this.scheduleRerun(needsRerun)
677678
}
678679
const onUnlink = (id: string) => {
679680
id = slash(id)
681+
this.logger.clearHighlightCache(id)
680682
this.invalidates.add(id)
681683

682684
if (this.state.filesMap.has(id)) {

packages/vitest/src/node/error.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ export async function printError(error: unknown, project: WorkspaceProject | und
6767
if (type)
6868
printErrorType(type, project.ctx)
6969
printErrorMessage(e, logger)
70+
if (e.codeFrame)
71+
logger.error(`${e.codeFrame}\n`)
7072

7173
// E.g. AssertionError from assert does not set showDiff but has both actual and expected properties
7274
if (e.diff)
@@ -80,7 +82,7 @@ export async function printError(error: unknown, project: WorkspaceProject | und
8082
printStack(project, stacks, nearest, errorProperties, (s) => {
8183
if (showCodeFrame && s === nearest && nearest) {
8284
const sourceCode = readFileSync(nearest.file, 'utf-8')
83-
logger.error(generateCodeFrame(sourceCode, 4, s))
85+
logger.error(generateCodeFrame(sourceCode.length > 100_000 ? sourceCode : logger.highlight(nearest.file, sourceCode), 4, s))
8486
}
8587
})
8688
}
@@ -123,6 +125,7 @@ const skipErrorProperties = new Set([
123125
'type',
124126
'showDiff',
125127
'diff',
128+
'codeFrame',
126129
'actual',
127130
'expected',
128131
'diffOptions',

packages/vitest/src/node/hoistMocks.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type { AwaitExpression, CallExpression, Identifier, ImportDeclaration, Va
55
import { findNodeAround } from 'acorn-walk'
66
import type { PluginContext } from 'rollup'
77
import { esmWalker } from '@vitest/utils/ast'
8+
import { highlight } from '@vitest/utils'
89
import { generateCodeFrame } from './error'
910

1011
export type Positioned<T> = T & {
@@ -256,7 +257,7 @@ export function hoistMocks(code: string, id: string, parse: PluginContext['parse
256257
name: 'SyntaxError',
257258
message: _error.message,
258259
stack: _error.stack,
259-
frame: generateCodeFrame(code, 4, insideCall.start + 1),
260+
frame: generateCodeFrame(highlight(code), 4, insideCall.start + 1),
260261
}
261262
throw error
262263
}

0 commit comments

Comments
 (0)