Skip to content

Commit 2a0af74

Browse files
authored
refactor: reuse codeFrame helper in logger and deduplicate code (#350)
* refactor: reuse codeFrame helper in logger and deduplicate code * chore: revert unnecessary change * chore: correct linting * chore: add missing changeset * test: update snapshots * fix: reuse already computed pos for ts location start * fix: apply changes from code review * fix: rename locationToBabelLocation to lineColLocToBabelLoc
1 parent b4da388 commit 2a0af74

File tree

5 files changed

+76
-116
lines changed

5 files changed

+76
-116
lines changed

.changeset/violet-moons-shake.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'vite-plugin-checker': patch
3+
---
4+
5+
refactor: reuse codeFrame helper in logger and deduplicate code
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,31 @@
11
import { describe, expect, it } from 'vitest'
22

3-
import type { SourceLocation } from '@babel/code-frame'
4-
5-
import { tsLocationToBabelLocation } from '../src/codeFrame'
3+
import { lineColLocToBabelLoc, tsLikeLocToBabelLoc } from '../src/codeFrame'
64

75
describe('code frame', () => {
86
it('should add 1 offset to TS location', () => {
9-
const babelLoc = tsLocationToBabelLocation({
7+
const babelLoc = tsLikeLocToBabelLoc({
108
start: { line: 1, character: 2 },
119
end: { line: 3, character: 4 },
1210
})
1311

1412
expect(babelLoc).toEqual({
1513
start: { line: 2, column: 3 },
1614
end: { line: 4, column: 5 },
17-
} as SourceLocation)
15+
})
16+
})
17+
18+
it('transform location without offset', () => {
19+
const babelLoc = lineColLocToBabelLoc({
20+
line: 1,
21+
column: 2,
22+
endLine: 3,
23+
endColumn: 4,
24+
})
25+
26+
expect(babelLoc).toEqual({
27+
start: { line: 1, column: 2 },
28+
end: { line: 3, column: 4 },
29+
})
1830
})
1931
})
+23-14
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,39 @@
11
import os from 'node:os'
2-
import type ts from 'typescript'
32

43
import { type SourceLocation, codeFrameColumns } from '@babel/code-frame'
54

6-
export function createFrame({
7-
source,
8-
location,
9-
}: {
10-
source: string // file source code
11-
location: SourceLocation
12-
}) {
13-
const frame = codeFrameColumns(source, location, {
14-
highlightCode: true,
5+
/**
6+
* Create a code frame from source code and location
7+
* @param source source code
8+
* @param location babel compatible location to highlight
9+
*/
10+
export function createFrame(source: string, location: SourceLocation): string {
11+
return codeFrameColumns(source, location, {
12+
// worker tty did not fork parent process stdout, let's make a workaround
13+
forceColor: true,
1514
})
1615
.split('\n')
1716
.map((line) => ` ${line}`)
1817
.join(os.EOL)
19-
20-
return frame
2118
}
2219

23-
export function tsLocationToBabelLocation(
24-
tsLoc: Record<'start' | 'end', ts.LineAndCharacter /** 0-based */>
20+
export function tsLikeLocToBabelLoc(
21+
tsLoc: Record<'start' | 'end', { line: number; character: number } /** 0-based */>
2522
): SourceLocation {
2623
return {
2724
start: { line: tsLoc.start.line + 1, column: tsLoc.start.character + 1 },
2825
end: { line: tsLoc.end.line + 1, column: tsLoc.end.character + 1 },
2926
}
3027
}
28+
29+
export function lineColLocToBabelLoc(d: {
30+
line: number
31+
column: number
32+
endLine?: number
33+
endColumn?: number
34+
}): SourceLocation {
35+
return {
36+
start: { line: d.line, column: d.column },
37+
end: { line: d.endLine || 0, column: d.endColumn },
38+
}
39+
}

packages/vite-plugin-checker/src/logger.ts

+15-81
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,10 @@ import * as _vscodeUri from 'vscode-uri'
1111
const URI = _vscodeUri?.default?.URI ?? _vscodeUri.URI
1212
import { parentPort } from 'node:worker_threads'
1313

14-
import { type SourceLocation, codeFrameColumns } from '@babel/code-frame'
14+
import type { SourceLocation } from '@babel/code-frame'
1515

1616
import { WS_CHECKER_ERROR_EVENT } from './client/index.js'
17+
import { createFrame, lineColLocToBabelLoc, tsLikeLocToBabelLoc } from './codeFrame.js'
1718
import {
1819
ACTION_TYPES,
1920
type ClientDiagnosticPayload,
@@ -25,14 +26,12 @@ import { isMainThread } from './utils.js'
2526
const _require = createRequire(import.meta.url)
2627
import type { ESLint } from 'eslint'
2728
import type Stylelint from 'stylelint'
28-
import type { Range } from 'vscode-languageclient'
2929
import type {
3030
Diagnostic as LspDiagnostic,
3131
PublishDiagnosticsParams,
3232
} from 'vscode-languageclient/node'
3333

3434
import type {
35-
LineAndCharacter,
3635
Diagnostic as TsDiagnostic,
3736
flattenDiagnosticMessageText as flattenDiagnosticMessageTextType,
3837
} from 'typescript'
@@ -162,34 +161,6 @@ export function toClientPayload(
162161
}
163162
}
164163

165-
export function createFrame({
166-
source,
167-
location,
168-
}: {
169-
/** file source code */
170-
source: string
171-
location: SourceLocation
172-
}) {
173-
const frame = codeFrameColumns(source, location, {
174-
// worker tty did not fork parent process stdout, let's make a workaround
175-
forceColor: true,
176-
})
177-
.split('\n')
178-
.map((line) => ` ${line}`)
179-
.join(os.EOL)
180-
181-
return frame
182-
}
183-
184-
export function tsLocationToBabelLocation(
185-
tsLoc: Record<'start' | 'end', LineAndCharacter /** 0-based */>
186-
): SourceLocation {
187-
return {
188-
start: { line: tsLoc.start.line + 1, column: tsLoc.start.character + 1 },
189-
end: { line: tsLoc.end.line + 1, column: tsLoc.end.character + 1 },
190-
}
191-
}
192-
193164
export function wrapCheckerSummary(checkerName: string, rawSummary: string): string {
194165
return `[${checkerName}] ${rawSummary}`
195166
}
@@ -224,18 +195,15 @@ export function normalizeTsDiagnostic(d: TsDiagnostic): NormalizedDiagnostic {
224195
let loc: SourceLocation | undefined
225196
const pos = d.start === undefined ? null : d.file?.getLineAndCharacterOfPosition?.(d.start)
226197
if (pos && d.file && typeof d.start === 'number' && typeof d.length === 'number') {
227-
loc = tsLocationToBabelLocation({
228-
start: d.file?.getLineAndCharacterOfPosition(d.start),
229-
end: d.file?.getLineAndCharacterOfPosition(d.start + d.length),
198+
loc = tsLikeLocToBabelLoc({
199+
start: pos,
200+
end: d.file.getLineAndCharacterOfPosition(d.start + d.length),
230201
})
231202
}
232203

233204
let codeFrame: string | undefined
234205
if (loc) {
235-
codeFrame = createFrame({
236-
source: d.file!.text,
237-
location: loc,
238-
})
206+
codeFrame = createFrame(d.file!.text, loc)
239207
}
240208

241209
return {
@@ -262,8 +230,8 @@ export function normalizeLspDiagnostic({
262230
fileText: string
263231
}): NormalizedDiagnostic {
264232
let level = DiagnosticLevel.Error
265-
const loc = lspRange2Location(diagnostic.range)
266-
const codeFrame = codeFrameColumns(fileText, loc)
233+
const loc = tsLikeLocToBabelLoc(diagnostic.range)
234+
const codeFrame = createFrame(fileText, loc)
267235

268236
switch (diagnostic.severity) {
269237
case 1: // Error
@@ -315,19 +283,6 @@ export function uriToAbsPath(documentUri: string): string {
315283
return URI.parse(documentUri).fsPath
316284
}
317285

318-
export function lspRange2Location(range: Range): SourceLocation {
319-
return {
320-
start: {
321-
line: range.start.line + 1,
322-
column: range.start.character + 1,
323-
},
324-
end: {
325-
line: range.end.line + 1,
326-
column: range.end.character + 1,
327-
},
328-
}
329-
}
330-
331286
/* --------------------------------- vue-tsc -------------------------------- */
332287

333288
export function normalizeVueTscDiagnostic(d: TsDiagnostic): NormalizedDiagnostic {
@@ -360,21 +315,9 @@ export function normalizeEslintDiagnostic(diagnostic: ESLint.LintResult): Normal
360315
break
361316
}
362317

363-
const loc: SourceLocation = {
364-
start: {
365-
line: d.line,
366-
column: d.column,
367-
},
368-
end: {
369-
line: d.endLine || 0,
370-
column: d.endColumn,
371-
},
372-
}
318+
const loc = lineColLocToBabelLoc(d)
373319

374-
const codeFrame = createFrame({
375-
source: diagnostic.source ?? '',
376-
location: loc,
377-
})
320+
const codeFrame = createFrame(diagnostic.source ?? '', loc)
378321

379322
return {
380323
message: `${d.message} (${d.ruleId})`,
@@ -410,22 +353,13 @@ export function normalizeStylelintDiagnostic(
410353
return null
411354
}
412355

413-
const loc: SourceLocation = {
414-
start: {
415-
line: d.line,
416-
column: d.column,
417-
},
418-
end: {
419-
line: d.endLine || 0,
420-
column: d.endColumn,
421-
},
422-
}
356+
const loc = lineColLocToBabelLoc(d)
423357

424-
const codeFrame = createFrame({
358+
const codeFrame = createFrame(
425359
// @ts-ignore
426-
source: diagnostic._postcssResult.css ?? '',
427-
location: loc,
428-
})
360+
diagnostic._postcssResult.css ?? '',
361+
loc
362+
)
429363

430364
return {
431365
message: `${d.text} (${d.rule})`,

playground/vls-vue2/__tests__/__snapshots__/test.spec.ts.snap

+16-16
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,38 @@
11
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
22

3-
exports[`vue2-vls > serve > get initial error and subsequent error 1`] = `"[{"checkerId":"VLS","frame":" 1 | <template>/n 2 | <div class=/"hello/">/n> 3 | <h1>{{ msg1 }}</h1>/n | ^^^^/n 4 | <p>/n 5 | For a guide and recipes on how to configure / customize this project,<br />/n 6 | check out the","id":"<PROJECT_ROOT>/playground-temp/vls-vue2/src/components/HelloWorld.vue","level":1,"loc":{"column":12,"file":"<PROJECT_ROOT>/playground-temp/vls-vue2/src/components/HelloWorld.vue","line":3},"message":"Property 'msg1' does not exist on type 'CombinedVueInstance<ExtractComputedReturns<{}> & { msg: string; } & Vue<Record<string, any>, Record<string, any>, never, never, (event: string, ...args: any[]) => Vue<...>> & ShallowUnwrapRef<...> & Vue<...>, ... 7 more ..., OptionTypesType<...>>'. Did you mean 'msg'?","stack":""}]"`;
3+
exports[`vue2-vls > serve > get initial error and subsequent error 1`] = `"[{"checkerId":"VLS","frame":" 1 | <template>/n 2 | <div class=/"hello/">/n > 3 | <h1>{{ msg1 }}</h1>/n | ^^^^/n 4 | <p>/n 5 | For a guide and recipes on how to configure / customize this project,<br />/n 6 | check out the","id":"<PROJECT_ROOT>/playground-temp/vls-vue2/src/components/HelloWorld.vue","level":1,"loc":{"column":12,"file":"<PROJECT_ROOT>/playground-temp/vls-vue2/src/components/HelloWorld.vue","line":3},"message":"Property 'msg1' does not exist on type 'CombinedVueInstance<ExtractComputedReturns<{}> & { msg: string; } & Vue<Record<string, any>, Record<string, any>, never, never, (event: string, ...args: any[]) => Vue<...>> & ShallowUnwrapRef<...> & Vue<...>, ... 7 more ..., OptionTypesType<...>>'. Did you mean 'msg'?","stack":""}]"`;
44

55
exports[`vue2-vls > serve > get initial error and subsequent error 2`] = `
66
[
77
" ERROR(VLS) Property 'msg1' does not exist on type 'CombinedVueInstance<ExtractComputedReturns<{}> & { msg: string; } & Vue<Record<string, any>, Record<string, any>, never, never, (event: string, ...args: any[]) => Vue<...>> & ShallowUnwrapRef<...> & Vue<...>, ... 7 more ..., OptionTypesType<...>>'. Did you mean 'msg'?
88
FILE <PROJECT_ROOT>/playground-temp/vls-vue2/src/components/HelloWorld.vue:3:12
99
10-
1 | <template>
11-
2 | <div class="hello">
12-
> 3 | <h1>{{ msg1 }}</h1>
13-
| ^^^^
14-
4 | <p>
15-
5 | For a guide and recipes on how to configure / customize this project,<br />
16-
6 | check out the
10+
1 | <template>
11+
2 | <div class="hello">
12+
> 3 | <h1>{{ msg1 }}</h1>
13+
| ^^^^
14+
4 | <p>
15+
5 | For a guide and recipes on how to configure / customize this project,<br />
16+
6 | check out the
1717
",
1818
"[VLS] Found 1 error and 0 warning",
1919
]
2020
`;
2121
22-
exports[`vue2-vls > serve > get initial error and subsequent error 3`] = `"[{"checkerId":"VLS","frame":" 1 | <template>/n 2 | <div class=/"hello/">/n> 3 | <h1>{{ msg1 }}</h1>/n | ^^^^/n 4 | <p>/n 5 | For a guide and recipes on how to configure / customize this project,<br />/n 6 | check out the","id":"<PROJECT_ROOT>/playground-temp/vls-vue2/src/components/HelloWorld.vue","level":1,"loc":{"column":12,"file":"<PROJECT_ROOT>/playground-temp/vls-vue2/src/components/HelloWorld.vue","line":3},"message":"Property 'msg1' does not exist on type 'CombinedVueInstance<ExtractComputedReturns<{}> & { msg: string; } & Vue<Record<string, any>, Record<string, any>, never, never, (event: string, ...args: any[]) => Vue<...>> & ShallowUnwrapRef<...> & Vue<...>, ... 7 more ..., OptionTypesType<...>>'. Did you mean 'msg'?","stack":""},{"checkerId":"VLS","frame":" 1 | <template>/n 2 | <div class=/"hello/">/n> 3 | <h1>{{ msg2 }}</h1>/n | ^^^^/n 4 | <p>/n 5 | For a guide and recipes on how to configure / customize this project,<br />/n 6 | check out the","id":"<PROJECT_ROOT>/playground-temp/vls-vue2/src/components/HelloWorld.vue","level":1,"loc":{"column":12,"file":"<PROJECT_ROOT>/playground-temp/vls-vue2/src/components/HelloWorld.vue","line":3},"message":"Property 'msg2' does not exist on type 'CombinedVueInstance<ExtractComputedReturns<{}> & { msg: string; } & Vue<Record<string, any>, Record<string, any>, never, never, (event: string, ...args: any[]) => Vue<...>> & ShallowUnwrapRef<...> & Vue<...>, ... 7 more ..., OptionTypesType<...>>'. Did you mean 'msg'?","stack":""}]"`;
22+
exports[`vue2-vls > serve > get initial error and subsequent error 3`] = `"[{"checkerId":"VLS","frame":" 1 | <template>/n 2 | <div class=/"hello/">/n > 3 | <h1>{{ msg1 }}</h1>/n | ^^^^/n 4 | <p>/n 5 | For a guide and recipes on how to configure / customize this project,<br />/n 6 | check out the","id":"<PROJECT_ROOT>/playground-temp/vls-vue2/src/components/HelloWorld.vue","level":1,"loc":{"column":12,"file":"<PROJECT_ROOT>/playground-temp/vls-vue2/src/components/HelloWorld.vue","line":3},"message":"Property 'msg1' does not exist on type 'CombinedVueInstance<ExtractComputedReturns<{}> & { msg: string; } & Vue<Record<string, any>, Record<string, any>, never, never, (event: string, ...args: any[]) => Vue<...>> & ShallowUnwrapRef<...> & Vue<...>, ... 7 more ..., OptionTypesType<...>>'. Did you mean 'msg'?","stack":""},{"checkerId":"VLS","frame":" 1 | <template>/n 2 | <div class=/"hello/">/n > 3 | <h1>{{ msg2 }}</h1>/n | ^^^^/n 4 | <p>/n 5 | For a guide and recipes on how to configure / customize this project,<br />/n 6 | check out the","id":"<PROJECT_ROOT>/playground-temp/vls-vue2/src/components/HelloWorld.vue","level":1,"loc":{"column":12,"file":"<PROJECT_ROOT>/playground-temp/vls-vue2/src/components/HelloWorld.vue","line":3},"message":"Property 'msg2' does not exist on type 'CombinedVueInstance<ExtractComputedReturns<{}> & { msg: string; } & Vue<Record<string, any>, Record<string, any>, never, never, (event: string, ...args: any[]) => Vue<...>> & ShallowUnwrapRef<...> & Vue<...>, ... 7 more ..., OptionTypesType<...>>'. Did you mean 'msg'?","stack":""}]"`;
2323
2424
exports[`vue2-vls > serve > get initial error and subsequent error 4`] = `
2525
[
2626
" ERROR(VLS) Property 'msg2' does not exist on type 'CombinedVueInstance<ExtractComputedReturns<{}> & { msg: string; } & Vue<Record<string, any>, Record<string, any>, never, never, (event: string, ...args: any[]) => Vue<...>> & ShallowUnwrapRef<...> & Vue<...>, ... 7 more ..., OptionTypesType<...>>'. Did you mean 'msg'?
2727
FILE <PROJECT_ROOT>/playground-temp/vls-vue2/src/components/HelloWorld.vue:3:12
2828
29-
1 | <template>
30-
2 | <div class="hello">
31-
> 3 | <h1>{{ msg2 }}</h1>
32-
| ^^^^
33-
4 | <p>
34-
5 | For a guide and recipes on how to configure / customize this project,<br />
35-
6 | check out the
29+
1 | <template>
30+
2 | <div class="hello">
31+
> 3 | <h1>{{ msg2 }}</h1>
32+
| ^^^^
33+
4 | <p>
34+
5 | For a guide and recipes on how to configure / customize this project,<br />
35+
6 | check out the
3636
",
3737
"[VLS] Found 1 error and 0 warning",
3838
]

0 commit comments

Comments
 (0)