Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add basic support for Biome #316

Merged
merged 8 commits into from
Jul 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/new-kangaroos-hang.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'vite-plugin-checker': patch
---

Added basic support for Biome
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"vetur.experimental.templateInterpolationService": true,
"typescript.tsdk": "node_modules/typescript/lib"
"typescript.tsdk": "node_modules/typescript/lib",
"vitest.disableWorkspaceWarning": true
}
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Visit [documentation](https://vite-plugin-checker.netlify.app) for usage

A Vite plugin that can run TypeScript, VLS, vue-tsc, ESLint, Stylelint in worker thread.
A Vite plugin that can run TypeScript, VLS, vue-tsc, ESLint, Biome, Stylelint in worker thread.

<p align="center">
<img alt="screenshot" src="https://user-images.githubusercontent.com/12322740/152739742-7444ee62-9ca7-4379-8f02-495c612ecc5c.png">
Expand Down
3 changes: 2 additions & 1 deletion docs/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { defineConfig } from 'vitepress'
export default defineConfig({
lang: 'en-US',
title: 'vite-plugin-checker',
description: 'Vite plugin that provide checks of TypeScript, ESLint, vue-tsc, and more.',
description: 'Vite plugin that provide checks of TypeScript, ESLint, Biome, vue-tsc, and more.',
lastUpdated: true,
themeConfig: {
outline: 'deep',
Expand Down Expand Up @@ -46,6 +46,7 @@ function sidebar() {
{ text: 'TypeScript', link: '/checkers/typescript' },
{ text: 'vue-tsc', link: '/checkers/vue-tsc' },
{ text: 'ESLint', link: '/checkers/eslint' },
{ text: 'Biome', link: '/checkers/biome' },
{ text: 'Stylelint', link: '/checkers/stylelint' },
{ text: 'VLS', link: '/checkers/vls' },
],
Expand Down
34 changes: 34 additions & 0 deletions docs/checkers/biome.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Biome

## Installation

1. Make sure [@biomejs/biome](https://www.npmjs.com/package/@biomejs/biome) is installed as peer dependency.

2. Add `biome` field to plugin config. The exact command to be run can be further configured with `command` and `flags` parameters. See [the documentation](https://biomejs.dev/reference/cli/) for CLI reference. The default root of the command uses Vite's [root](https://vitejs.dev/config/#root).

:::tip
Do not add `--apply` to the flags since the plugin is only aiming at checking issues.
:::

```js
// e.g.
export default {
plugins: [
checker({
biome: {
command: 'check',
},
}),
],
}
```

## Configuration

Advanced object configuration table of `options.biome`

| field | Type | Default value | Description |
| :----------- | --------------------------------------- | ----------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| command | `'check' \| 'lint' \| 'format' \| 'ci'` | `'lint'` in dev, `'check'` in build | The command to execute biome with |
| flags | `string` | `''` | CLI flags to pass to the command |
| dev.logLevel | `('error' \| 'warning')[]` | `['error', 'warning']` | **(Only in dev mode)** Which level of Biome diagnostics should be emitted to terminal and overlay in dev mode |
2 changes: 1 addition & 1 deletion docs/checkers/overview.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Checkers overview

vite-plugin-checkers provide built-in checkers. For now, it supports [TypeScript](/checkers/typescript), [ESLint](/checkers/eslint), [vue-tsc](/checkers/vue-tsc), [VLS](/checkers/vls), [Stylelint](/checkers/stylelint).
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).

## How to add a checker

Expand Down
1 change: 1 addition & 0 deletions packages/runtime/src/components/Diagnostic.ce.vue
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ function calcLink(text: string) {
const checkerColorMap: Record<string, string> = {
TypeScript: '#3178c6',
ESLint: '#7b7fe3',
Biome: '#60a5fa',
VLS: '#64b587',
'vue-tsc': '#64b587',
Stylelint: '#ffffff',
Expand Down
5 changes: 5 additions & 0 deletions packages/vite-plugin-checker/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
"vscode-uri": "^3.0.2"
},
"peerDependencies": {
"@biomejs/biome": ">=1.7",
"eslint": ">=7",
"meow": "^9.0.0",
"optionator": "^0.9.1",
Expand All @@ -66,6 +67,9 @@
"vue-tsc": ">=2.0.0"
},
"peerDependenciesMeta": {
"@biomejs/biome": {
"optional": true
},
"eslint": {
"optional": true
},
Expand All @@ -92,6 +96,7 @@
}
},
"devDependencies": {
"@biomejs/biome": "^1.8.3",
"@types/eslint": "^7.2.14",
"@types/fs-extra": "^11.0.1",
"@vue/language-core": "^2.0.14",
Expand Down
8 changes: 1 addition & 7 deletions packages/vite-plugin-checker/src/FileDiagnosticManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,12 @@ import type { NormalizedDiagnostic } from './logger.js'

class FileDiagnosticManager {
public diagnostics: NormalizedDiagnostic[] = []
private initialized = false

/**
* Only used when initializing the manager
* Initialize and reset the diagnostics array
*/
public initWith(diagnostics: NormalizedDiagnostic[]) {
if (this.initialized) {
throw new Error('FileDiagnosticManager is already initialized')
}

this.diagnostics = [...diagnostics]
this.initialized = true
}

public getDiagnostics(fileName?: string) {
Expand Down
85 changes: 85 additions & 0 deletions packages/vite-plugin-checker/src/checkers/biome/cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { exec } from 'node:child_process'
import path from 'node:path'
import strip from 'strip-ansi'
import { createFrame } from '../../codeFrame.js'
import type { NormalizedDiagnostic } from '../../logger.js'
import { DiagnosticLevel } from '../../types.js'
import type { BiomeOutput } from './types.js'

export const severityMap = {
error: DiagnosticLevel.Error,
warning: DiagnosticLevel.Warning,
info: DiagnosticLevel.Suggestion,
} as const

export function getBiomeCommand(command: string, flags: string, files: string) {
const defaultFlags = '--reporter json'
return ['biome', command || 'lint', flags, defaultFlags, files].filter(Boolean).join(' ')
}

export function runBiome(command: string, cwd: string) {
return new Promise<NormalizedDiagnostic[]>((resolve, reject) => {
exec(
command,
{
cwd,
},
(error, stdout, stderr) => {
resolve([...parseBiomeOutput(stdout)])
}
)
})
}

function parseBiomeOutput(output: string) {
let parsed: BiomeOutput
try {
parsed = JSON.parse(output)
} catch (e) {
return []
}

const diagnostics: NormalizedDiagnostic[] = parsed.diagnostics.map((d) => {
let file = d.location.path?.file
if (file) file = path.normalize(file)

const loc = {
file: file || '',
start: getLineAndColumn(d.location.sourceCode, d.location.span?.[0]),
end: getLineAndColumn(d.location.sourceCode, d.location.span?.[1]),
}

const codeFrame = createFrame(d.location.sourceCode || '', loc)

return {
message: `[${d.category}] ${d.description}`,
conclusion: '',
level: severityMap[d.severity as keyof typeof severityMap] ?? DiagnosticLevel.Error,
checker: 'Biome',
id: file,
codeFrame,
stripedCodeFrame: codeFrame && strip(codeFrame),
loc,
}
})

return diagnostics
}

function getLineAndColumn(text?: string, offset?: number) {
if (!text || !offset) return { line: 0, column: 0 }

let line = 1
let column = 1

for (let i = 0; i < offset; i++) {
if (text[i] === '\n') {
line++
column = 1
} else {
column++
}
}

return { line, column }
}
147 changes: 147 additions & 0 deletions packages/vite-plugin-checker/src/checkers/biome/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import { parentPort } from 'node:worker_threads'
import chokidar from 'chokidar'
import { Checker } from '../../Checker.js'
import { FileDiagnosticManager } from '../../FileDiagnosticManager.js'
import {
composeCheckerSummary,
consoleLog,
diagnosticToRuntimeError,
diagnosticToTerminalLog,
filterLogLevel,
toClientPayload,
} from '../../logger.js'
import { ACTION_TYPES, type CreateDiagnostic, DiagnosticLevel } from '../../types.js'
import { getBiomeCommand, runBiome, severityMap } from './cli.js'

const __filename = fileURLToPath(import.meta.url)

const manager = new FileDiagnosticManager()
let createServeAndBuild: any

const createDiagnostic: CreateDiagnostic<'biome'> = (pluginConfig) => {
let overlay = true
let terminal = true

let command = ''
let flags = ''

if (typeof pluginConfig.biome === 'object') {
command = pluginConfig.biome.command || ''
flags = pluginConfig.biome.flags || ''
}

return {
config: async ({ enableOverlay, enableTerminal }) => {
overlay = enableOverlay
terminal = enableTerminal
},
async configureServer({ root }) {
if (!pluginConfig.biome) return

const logLevel = (() => {
if (typeof pluginConfig.biome !== 'object') return undefined
const userLogLevel = pluginConfig.biome.dev?.logLevel
if (!userLogLevel) return undefined

return userLogLevel.map((l) => severityMap[l])
})()

const dispatchDiagnostics = () => {
const diagnostics = filterLogLevel(manager.getDiagnostics(), logLevel)

if (terminal) {
for (const d of diagnostics) {
consoleLog(diagnosticToTerminalLog(d, 'Biome'))
}

const errorCount = diagnostics.filter((d) => d.level === DiagnosticLevel.Error).length
const warningCount = diagnostics.filter((d) => d.level === DiagnosticLevel.Warning).length
consoleLog(composeCheckerSummary('Biome', errorCount, warningCount))
}

if (overlay) {
parentPort?.postMessage({
type: ACTION_TYPES.overlayError,
payload: toClientPayload(
'biome',
diagnostics.map((d) => diagnosticToRuntimeError(d))
),
})
}
}

const handleFileChange = async (filePath: string, type: 'change' | 'unlink') => {
const absPath = path.resolve(root, filePath)
if (type === 'unlink') {
manager.updateByFileId(absPath, [])
} else if (type === 'change') {
const isConfigFile = path.basename(absPath) === 'biome.json'

if (isConfigFile) {
const runCommand = getBiomeCommand(command, flags, root)
const diagnostics = await runBiome(runCommand, root)
manager.initWith(diagnostics)
} else {
const runCommand = getBiomeCommand(command, flags, absPath)
const diagnosticsOfChangedFile = await runBiome(runCommand, root)
manager.updateByFileId(absPath, diagnosticsOfChangedFile)
}
}

dispatchDiagnostics()
}

// initial check
const runCommand = getBiomeCommand(command, flags, root)
const diagnostics = await runBiome(runCommand, root)

manager.initWith(diagnostics)
dispatchDiagnostics()

// watch lint
const watcher = chokidar.watch([], {
cwd: root,
ignored: (path: string) => path.includes('node_modules'),
})
watcher.on('change', async (filePath) => {
handleFileChange(filePath, 'change')
})
watcher.on('unlink', async (filePath) => {
handleFileChange(filePath, 'unlink')
})
watcher.add('.')
},
}
}

export class BiomeChecker extends Checker<'biome'> {
public constructor() {
super({
name: 'biome',
absFilePath: __filename,
build: {
buildBin: (pluginConfig) => {
if (typeof pluginConfig.biome === 'object') {
const { command, flags } = pluginConfig.biome
return ['biome', [command || 'lint', flags || ''] as const]
}
return ['biome', ['lint']]
},
},
createDiagnostic,
})
}

public init() {
const _createServeAndBuild = super.initMainThread()
createServeAndBuild = _createServeAndBuild
super.initWorkerThread()
}
}

export { createServeAndBuild }
const biomeChecker = new BiomeChecker()
biomeChecker.prepare()
biomeChecker.init()
Loading