Skip to content

Commit

Permalink
feat: add basic support for Biome (#316)
Browse files Browse the repository at this point in the history
* feat: add basic support for Biome

* add changeset

* fix: tweak

* chore: should be patch

* fix: vitest

* fix: Windows

* update windows snapshots

* normalize path returned by biome

---------

Co-authored-by: fi3ework <[email protected]>
  • Loading branch information
KurtGokhan and fi3ework committed Jul 9, 2024
1 parent 92b63b2 commit 568b782
Show file tree
Hide file tree
Showing 25 changed files with 861 additions and 15 deletions.
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

0 comments on commit 568b782

Please sign in to comment.