|
| 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