Skip to content

Commit

Permalink
feat: support ESLint serve
Browse files Browse the repository at this point in the history
  • Loading branch information
fi3ework committed Jul 17, 2021
1 parent a855866 commit 98aca8c
Show file tree
Hide file tree
Showing 8 changed files with 125 additions and 47 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -68,12 +68,15 @@ export async function killServer() {

export async function pollingUntil<T>(poll: () => Promise<T>, until: (actual: T) => boolean) {
const maxTries = process.env.CI ? 1000 : 100 // 50s / 5s
for (let tries = 0; tries < maxTries; tries++) {
for (let tries = 0; tries <= maxTries; tries++) {
const actual = await poll()
if (until(actual)) {
break
} else {
await sleep(50)
if (tries === maxTries) {
console.log('reach max polling tries')
}
}
}
}
Expand Down
4 changes: 3 additions & 1 deletion packages/vite-plugin-checker/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,11 @@
"homepage": "https://github.com/fi3ework/vite-plugin-checker",
"dependencies": {
"@babel/code-frame": "^7.12.13",
"@types/lodash.pick": "^4.4.6",
"ansi-escapes": "^4.3.0",
"chokidar": "^3.5.1",
"fast-glob": "^3.2.7",
"lodash.pick": "^4.4.0",
"lodash.debounce": "^4.0.8",
"npm-run-path": "^4.0.1",
"strip-ansi": "^6.0.0",
"tiny-invariant": "^1.1.0",
Expand All @@ -50,6 +50,8 @@
"vite": "^2.0.0"
},
"devDependencies": {
"@types/lodash.pick": "^4.4.6",
"@types/lodash.debounce": "^4.0.6",
"@types/eslint": "^7.2.14",
"commander": "^8.0.0",
"vls": "^0.7.2"
Expand Down
82 changes: 68 additions & 14 deletions packages/vite-plugin-checker/src/checkers/eslint/main.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,82 @@
import { ESLint } from 'eslint'
// import debounce from 'lodash.debounce'
import os from 'os'
import path from 'path'
import invariant from 'tiny-invariant'
import { ESLint } from 'eslint'
import { parentPort } from 'worker_threads'

import { Checker } from '../../Checker'
import {
diagnosticToTerminalLog,
diagnosticToViteError,
ensureCall,
NormalizedDiagnostic,
normalizeEslintDiagnostic,
} from '../../logger'

import type { CreateDiagnostic } from '../../types'
import type { ErrorPayload } from 'vite'

function findOneErrorFromCache() {}

const createDiagnostic: CreateDiagnostic<'eslint'> = (pluginConfig) => {
let overlay = true // Vite defaults to true
let currErr: ErrorPayload['err'] | null = null

return {
config: async ({ hmr }) => {
const eslint = new ESLint()
const diagnostics = await eslint.lintFiles(path.resolve(process.cwd(), 'src/*.ts'))
const normalized = diagnostics.map((p) => normalizeEslintDiagnostic(p)).flat(1)
normalized.forEach((n) => {
console.log(diagnosticToTerminalLog(n))
const viteOverlay = !(typeof hmr === 'object' && hmr.overlay === false)
if (pluginConfig.overlay === false || !viteOverlay) {
overlay = false
}
},
async configureServer({ root }) {
if (!pluginConfig.eslint) return

const extensions = pluginConfig.eslint.ext ? pluginConfig.eslint?.ext.split(',') : undefined
const namedExtensions = extensions ?? ['.js']
const eslint = new ESLint({
extensions,
})
invariant(pluginConfig.eslint, 'config.eslint should not be `false`')
invariant(
pluginConfig.eslint.files,
`eslint.files is required, but got ${pluginConfig.eslint.files}`
)

const paths =
typeof pluginConfig.eslint.files === 'string'
? [pluginConfig.eslint.files]
: pluginConfig.eslint.files

const diagnosticsCache: Record<string, NormalizedDiagnostic[]> = {}

Checker.watcher.add(paths)
Checker.watcher.on('all', async (event, filePath) => {
if (!['add', 'change'].includes(event)) return
if (!namedExtensions.includes(path.extname(filePath))) return

const diagnostics = await eslint.lintFiles(filePath)
const normalized = diagnostics.map((p) => normalizeEslintDiagnostic(p)).flat(1)
normalized.forEach((n) => {
console.log(diagnosticToTerminalLog(n))
})
diagnosticsCache[filePath] = normalized

const lastErr = normalized[0]

if (!lastErr) return

if (overlay) {
parentPort?.postMessage({
type: 'ERROR',
payload: {
type: 'error',
err: diagnosticToViteError(lastErr),
},
})
}
})
},
configureServer({ root }) {},
}
}

Expand All @@ -38,12 +86,18 @@ export class EslintChecker extends Checker<'eslint'> {
name: 'typescript',
absFilePath: __filename,
build: {
buildBin: (userConfig) => {
invariant(
userConfig.eslint.files,
`eslint.files is required, but got ${userConfig.eslint.files}`
)
return ['eslint', ['--ext', userConfig.eslint.ext ?? '.js', userConfig.eslint.files]]
buildBin: (pluginConfig) => {
let ext = '.js'
let files: string[] = []
if (pluginConfig.eslint) {
ext = pluginConfig.eslint.ext ?? ext
files =
typeof pluginConfig.eslint.files === 'string'
? [pluginConfig.eslint.files]
: pluginConfig.eslint.files
}

return ['eslint', ['--ext', ext, ...files]]
},
},
createDiagnostic,
Expand Down
2 changes: 1 addition & 1 deletion packages/vite-plugin-checker/src/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import type {
LineAndCharacter,
} from 'typescript'

interface NormalizedDiagnostic {
export interface NormalizedDiagnostic {
/** error message */
message?: string
/** error conclusion */
Expand Down
2 changes: 1 addition & 1 deletion packages/vite-plugin-checker/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ function createCheckers(userConfig: UserPluginConfig, env: ConfigEnv): ServeAndB
return serveAndBuildCheckers
}

export default function Plugin(userConfig?: UserPluginConfig): Plugin {
export default function Plugin(userConfig: UserPluginConfig): Plugin {
const enableBuild = userConfig?.enableBuild ?? true
let checkers: ServeAndBuildChecker[] = []
let viteMode: ConfigEnv['command'] | undefined
Expand Down
13 changes: 9 additions & 4 deletions packages/vite-plugin-checker/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,16 +29,21 @@ export type VlsConfig =

/** ESLint checker configuration */
export type EslintConfig =
| boolean
| Partial<{
| false
| {
/** One or more glob patterns to the files that should be linted. Works the same as the eslint command. */
files: string | string[]
/**
* Specify JavaScript file extensions, e.g. '.jsx,.js'
* @defaultValue: .js
*/
ext?: string
}>
/**
* millisecond for watcher to wait to trigger re-lint
* @defaultValue: 300
*/
watchDelay?: number
}

/** checkers shared configuration */
export interface SharedConfig {
Expand Down Expand Up @@ -112,7 +117,7 @@ export type Actions = OverlayErrorAction | ConfigAction | ConfigureServerAction

export type BuildCheckBin = BuildCheckBinStr | BuildCheckBinFn
export type BuildCheckBinStr = [string, ReadonlyArray<string>]
export type BuildCheckBinFn = (config: any) => [string, ReadonlyArray<string>]
export type BuildCheckBinFn = (config: UserPluginConfig) => [string, ReadonlyArray<string>]

export interface ConfigureServeChecker {
worker: Worker
Expand Down
44 changes: 22 additions & 22 deletions playground/vanilla-ts/__tests__/test.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,36 +37,36 @@ describe('eslint', () => {

const snapshot = {
errorCode1: `var hello = 'Hello'`,
errorCode2: '',
errorCode2: `var hello = 'Hello~'`,
absPath: 'vanilla-ts/src/main.ts:3:1',
relativePath: 'src/main.ts:3:1',
errorMsg: `Unexpected var, use let or const instead.`,
}

// it('get initial error and subsequent error', async () => {
// await viteServe({ cwd: testDir })
// await pollingUntil(getHmrOverlay, (dom) => !!dom)
// const [message1, file1, frame1] = await getHmrOverlayText()
// expect(message1).toContain(snapshot.errorMsg)
// expect(file1).toContain(snapshot.absPath)
// expect(frame1).toContain(snapshot.errorCode1)
// expect(stripedLog).toContain(snapshot.errorCode1)
// expect(stripedLog).toContain(snapshot.errorMsg)
// expect(stripedLog).toContain(snapshot.relativePath)
it('get initial error and subsequent error', async () => {
await viteServe({ cwd: testDir })
await pollingUntil(getHmrOverlay, (dom) => !!dom)
const [message1, file1, frame1] = await getHmrOverlayText()
expect(message1).toContain(snapshot.errorMsg)
expect(file1).toContain(snapshot.absPath)
expect(frame1).toContain(snapshot.errorCode1)
expect(stripedLog).toContain(snapshot.errorCode1)
expect(stripedLog).toContain(snapshot.errorMsg)
expect(stripedLog).toContain(snapshot.relativePath)

// resetTerminalLog()
// editFile('src/App.tsx', (code) => code.replace('useState<string>(1)', 'useState<string>(2)'))
// await sleep(2000)
// const [, , frame2] = await getHmrOverlayText()
// expect(frame2).toContain(snapshot.errorCode2)
// expect(stripedLog).toContain(snapshot.errorCode2)
// })
resetTerminalLog()
editFile('src/main.ts', (code) => code.replace(`'Hello'`, `'Hello~'`))
await sleep(2000)
// the case will trigger a full reload, so HRM overlay will be flushed
await expect(getHmrOverlayText()).rejects.toThrow(
'<vite-error-overlay> shadow dom is expected to be found, but got null'
)
expect(stripedLog).toContain(snapshot.errorCode2)
})

it('overlay: false', async () => {
resetTerminalLog()
editFile('vite.config.ts', (code) =>
code.replace('eslint: true,', 'eslint: true, overlay: false,')
)
editFile('vite.config.ts', (code) => code.replace('eslint: {', 'overlay: false, eslint: {'))

await viteServe({ cwd: testDir })
await sleep(6000)
Expand All @@ -81,7 +81,7 @@ describe('eslint', () => {
resetTerminalLog()
editFile('src/main.ts', (code) => code.replace('var hello', 'const hello'))
await sleep(2000)
expect(stripedLog).toContain(snapshot.errorCode2)
expect(stripedLog).not.toContain(snapshot.errorCode2)
})
})

Expand Down
20 changes: 17 additions & 3 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 98aca8c

Please sign in to comment.