Skip to content

Commit 61408e7

Browse files
authored
feat(oxlint): add support for oxlint (#578)
1 parent 5ddc18c commit 61408e7

File tree

28 files changed

+940
-4
lines changed

28 files changed

+940
-4
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,9 @@ dist
103103
# TernJS port file
104104
.tern-port
105105

106+
# JetBrains IDEs
107+
.idea
108+
106109
#
107110
lib/
108111
playground-temp/

docs/.vitepress/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ function sidebar() {
5454
{ text: 'Biome', link: '/checkers/biome' },
5555
{ text: 'Stylelint', link: '/checkers/stylelint' },
5656
{ text: 'VLS', link: '/checkers/vls' },
57+
{ text: 'oxlint', link: '/checkers/oxlint' },
5758
],
5859
},
5960
{

docs/checkers/overview.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Checkers overview
22

3-
vite-plugin-checkers provide built-in checkers. For now, it supports [TypeScript](/checkers/typescript), [ESLint](/checkers/eslint), [Biome](/checkers/biome), [vue-tsc](/checkers/vue-tsc), [VLS](/checkers/vls), [Stylelint](/checkers/stylelint).
3+
vite-plugin-checkers provide built-in checkers. For now, it supports [TypeScript](/checkers/typescript), [ESLint](/checkers/eslint), [Biome](/checkers/biome), [vue-tsc](/checkers/vue-tsc), [VLS](/checkers/vls), [Stylelint](/checkers/stylelint), [oxlint](/checkers/oxlint).
44

55
## How to add a checker
66

docs/checkers/oxlint.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# oxlint
2+
3+
## Installation
4+
5+
1. Make sure [oxlint](https://www.npmjs.com/package/oxlint) is installed as a dependency.
6+
7+
2. Add `oxlint` field to plugin config. Valid options are either `true` or a configuration object.
8+
9+
```js
10+
// e.g.
11+
export default defineConfig({
12+
plugins: [
13+
checker({
14+
oxlint: true,
15+
// or
16+
oxlint: {
17+
lintCommand: "oxlint -D correctness",
18+
},
19+
}),
20+
],
21+
});
22+
```
23+
24+
## Configuration
25+
26+
Advanced object configuration table of `options.oxlint`
27+
28+
| field | Type | Default value | Description |
29+
| :----------------- | ---------------------------------------------------------------------------------------------------------- | ---------------------- |------------------------------------------------------------------------------------------------------------------------|
30+
| lintCommand | `string` | `oxlint` | `lintCommand` will be executed at build mode. |
31+
| watchPath | `string \| string[]` | `undefined` | **(Only in dev mode)** Configure path to watch files for oxlint. If not specified, will watch the entire project root. |
32+
| dev.logLevel | `('error' \| 'warning')[]` | `['error', 'warning']` | **(Only in dev mode)** Which level of oxlint should be emitted to terminal and overlay in dev mode. |
33+

docs/introduction/introduction.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# About vite-plugin-checker
22

3-
A Vite plugin that can run TypeScript, VLS, vue-tsc, ESLint, Stylelint in worker thread to **add type checking and linting support** for Vite.
3+
A Vite plugin that can run TypeScript, VLS, vue-tsc, ESLint,
4+
Stylelint and oxlint in worker thread to **add type checking and linting support** for Vite.
45

56
<div :style="{ 'display': 'flex' }">
67
<a href="https://www.npmjs.com/package/vite-plugin-checker" :style="{ 'margin-right': '4px' }"><img src="https://img.shields.io/npm/v/vite-plugin-checker" /></a>

knip.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
},
66
"workspaces": {
77
".": {
8-
"entry": ["scripts/*", "playground/*"]
8+
"entry": ["scripts/*", "playground/*"],
9+
"ignoreDependencies": ["oxlint"]
910
},
1011
"docs": {
1112
"entry": [".vitepress/config.ts"]

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
"fast-json-stable-stringify": "^2.1.0",
4141
"knip": "^5.62.0",
4242
"lint-staged": "^16.1.6",
43+
"oxlint": "^1.18.0",
4344
"pkg-pr-new": "^0.0.60",
4445
"playwright-chromium": "^1.54.2",
4546
"publint": "^0.3.13",

packages/runtime/src/components/Diagnostic.ce.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ const checkerColorMap: Record<string, string> = {
4141
VLS: '#64b587',
4242
'vue-tsc': '#64b587',
4343
Stylelint: '#ffffff',
44+
'oxlint': '#a8b1ff'
4445
} as const
4546
4647
const fileRE = /(?:[a-zA-Z]:\\|\/).*(:\d+:\d+)?/g

packages/vite-plugin-checker/package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
"eslint": ">=7",
6262
"meow": "^13.2.0",
6363
"optionator": "^0.9.4",
64+
"oxlint": ">=1",
6465
"stylelint": ">=16",
6566
"typescript": "*",
6667
"vite": ">=5.4.20",
@@ -81,6 +82,9 @@
8182
"optionator": {
8283
"optional": true
8384
},
85+
"oxlint": {
86+
"optional": true
87+
},
8488
"stylelint": {
8589
"optional": true
8690
},
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
import { exec } from 'node:child_process'
2+
import fs from 'node:fs/promises'
3+
import path from 'node:path'
4+
import colors from 'picocolors'
5+
import strip from 'strip-ansi'
6+
import { createFrame, offsetRangeToBabelLocation } from '../../codeFrame.js'
7+
import { consoleLog, type NormalizedDiagnostic } from '../../logger.js'
8+
import { DiagnosticLevel } from '../../types.js'
9+
import { parseArgsStringToArgv } from '../stylelint/argv.js'
10+
11+
const severityMap = {
12+
error: DiagnosticLevel.Error,
13+
warning: DiagnosticLevel.Warning,
14+
info: DiagnosticLevel.Suggestion,
15+
} as const
16+
17+
export function mapSeverity(s: string): DiagnosticLevel {
18+
return severityMap[s as keyof typeof severityMap] ?? DiagnosticLevel.Error
19+
}
20+
21+
export function getOxlintCommand(command: string) {
22+
const parsed = parseArgsStringToArgv(command)
23+
24+
const index = parsed.findIndex((p) => p === '--format' || p === '-f')
25+
if (index === -1) {
26+
parsed.push('--format', 'json')
27+
} else {
28+
consoleLog(
29+
colors.yellow(
30+
'vite-plugin-checker will force append "--format json" to the flags in dev mode, please don\'t use "--format" or "-f" flag in "config.oxlint.lintCommand".',
31+
),
32+
'warn',
33+
)
34+
35+
parsed.splice(index, 2, '--format', 'json')
36+
}
37+
38+
return parsed
39+
}
40+
41+
export function runOxlint(command: string, cwd: string) {
42+
return new Promise<NormalizedDiagnostic[]>((resolve, _reject) => {
43+
exec(
44+
command,
45+
{
46+
cwd,
47+
maxBuffer: Number.POSITIVE_INFINITY,
48+
},
49+
(_error, stdout, _stderr) => {
50+
parseOxlintOutput(stdout, cwd)
51+
.then(resolve)
52+
.catch(() => resolve([]))
53+
},
54+
)
55+
})
56+
}
57+
58+
type Span = { offset: number; length: number }
59+
type Entry = {
60+
file: string
61+
span: Span
62+
code: string
63+
message: string
64+
severity: string
65+
}
66+
67+
async function parseOxlintOutput(
68+
output: string,
69+
cwd: string,
70+
): Promise<NormalizedDiagnostic[]> {
71+
const parsed = safeParseOxlint(output)
72+
if (!parsed) return []
73+
74+
const entries = getEntries(parsed, cwd)
75+
if (entries.length === 0) return []
76+
77+
const files = getUniqueFiles(entries)
78+
const sourceCache = await readSources(files)
79+
80+
return buildDiagnostics(entries, sourceCache)
81+
}
82+
83+
function safeParseOxlint(output: string): OxlintOutput | null {
84+
try {
85+
return JSON.parse(output)
86+
} catch {
87+
return null
88+
}
89+
}
90+
91+
function getEntries(parsed: OxlintOutput, cwd: string) {
92+
return parsed.diagnostics.flatMap(
93+
({ filename, labels, code, message, severity }) => {
94+
const file = normalizePath(filename, cwd)
95+
96+
const [label] = labels
97+
if (!label) return []
98+
99+
return [
100+
{
101+
file,
102+
span: label.span,
103+
code,
104+
message,
105+
severity,
106+
},
107+
] as Entry[]
108+
},
109+
)
110+
}
111+
112+
function getUniqueFiles(entries: Entry[]) {
113+
return [...new Set(entries.map((e) => e.file))]
114+
}
115+
116+
async function readSources(files: string[]) {
117+
const cache = new Map<string, string>()
118+
await Promise.all(
119+
files.map(async (file) => {
120+
try {
121+
const source = await fs.readFile(file, 'utf8')
122+
cache.set(file, source)
123+
} catch {
124+
// Ignore unreadable files; related diagnostics will be skipped.
125+
}
126+
}),
127+
)
128+
return cache
129+
}
130+
131+
function buildDiagnostics(entries: Entry[], sources: Map<string, string>) {
132+
return entries.flatMap((entry) => {
133+
const source = sources.get(entry.file)
134+
if (!source) return []
135+
136+
const loc = offsetRangeToBabelLocation(
137+
source,
138+
entry.span.offset,
139+
entry.span.length,
140+
)
141+
const codeFrame = createFrame(source, loc)
142+
143+
return [
144+
{
145+
message: `${entry.code}: ${entry.message}`,
146+
conclusion: '',
147+
level: mapSeverity(entry.severity),
148+
checker: 'oxlint',
149+
id: entry.file,
150+
codeFrame,
151+
stripedCodeFrame: codeFrame && strip(codeFrame),
152+
loc,
153+
},
154+
] as NormalizedDiagnostic[]
155+
})
156+
}
157+
158+
function normalizePath(p: string, cwd: string) {
159+
let filename = p
160+
if (filename) {
161+
filename = path.isAbsolute(filename)
162+
? filename
163+
: path.resolve(cwd, filename)
164+
filename = path.normalize(filename)
165+
}
166+
167+
return filename
168+
}
169+
170+
type OxlintOutput = {
171+
diagnostics: Diagnostic[]
172+
number_of_files: number
173+
number_of_rules: number
174+
threads_count: number
175+
start_time: number
176+
}
177+
178+
type Diagnostic = {
179+
message: string
180+
code: string
181+
severity: string
182+
causes: unknown[]
183+
url: string
184+
help: string
185+
filename: string
186+
labels: Label[]
187+
related: unknown[]
188+
}
189+
190+
type Label = {
191+
label: string
192+
span: Record<'offset' | 'length' | 'line' | 'column', number>
193+
}

0 commit comments

Comments
 (0)