From d328db5f82b691e5155938438d3adbd697e58c3a Mon Sep 17 00:00:00 2001 From: Charan Rathore <180254320+charan-rathore@users.noreply.github.com> Date: Thu, 1 Oct 2026 09:30:10 +0530 Subject: [PATCH 1/2] fix: lint newly created files in dev watchers --- .../__tests__/newLintFile.spec.ts | 116 ++++++++++++++++++ .../src/checkers/biome/main.ts | 7 +- .../src/checkers/eslint/main.ts | 7 +- .../src/checkers/stylelint/main.ts | 7 +- 4 files changed, 131 insertions(+), 6 deletions(-) create mode 100644 packages/vite-plugin-checker/__tests__/newLintFile.spec.ts diff --git a/packages/vite-plugin-checker/__tests__/newLintFile.spec.ts b/packages/vite-plugin-checker/__tests__/newLintFile.spec.ts new file mode 100644 index 00000000..ca09e7cc --- /dev/null +++ b/packages/vite-plugin-checker/__tests__/newLintFile.spec.ts @@ -0,0 +1,116 @@ +import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises' +import path from 'node:path' +import { Worker } from 'node:worker_threads' +import { expect, it } from 'vitest' +import { ACTION_TYPES } from '../src/types' + +const cases = [ + { + name: 'eslint', + configFile: 'eslint.config.mjs', + config: + 'export default [{files: ["**/*.js"], rules: {"no-var": "error"}}]\n', + checker: { eslint: { lintCommand: 'eslint src', dev: { debounceMs: 10 } } }, + extension: 'js', + initial: 'const initial = 1;\n', + added: 'var added = 1;\n', + message: 'Unexpected var', + }, + { + name: 'stylelint', + configFile: '.stylelintrc.json', + config: JSON.stringify({ rules: { 'color-no-invalid-hex': true } }), + checker: { + stylelint: { + lintCommand: 'stylelint "src/*.css"', + dev: { debounceMs: 10 }, + }, + }, + extension: 'css', + initial: 'a { color: #fff; }\n', + added: 'a { color: #xyz; }\n', + message: 'Invalid hex color', + }, + { + name: 'biome', + configFile: 'biome.json', + config: JSON.stringify({ + formatter: { enabled: false }, + linter: { + rules: { recommended: false, suspicious: { noDebugger: 'error' } }, + }, + }), + checker: { biome: { command: 'lint', dev: { debounceMs: 10 } } }, + extension: 'js', + initial: 'const initial = 1;\n', + added: 'debugger;\n', + message: 'debugger', + }, +] + +it.each( + cases, +)('reports $name diagnostics for a newly added file without a second save', async (entry) => { + const root = await mkdtemp(path.join(import.meta.dirname, 'lint-add-')) + const messages: unknown[] = [] + let worker: Worker | undefined + let workerError: Error | undefined + try { + await mkdir(path.join(root, 'src')) + await writeFile( + path.join(root, `src/initial.${entry.extension}`), + entry.initial, + ) + await writeFile(path.join(root, entry.configFile), entry.config) + worker = new Worker( + new URL(`../dist/checkers/${entry.name}/main.js`, import.meta.url), + { + workerData: { + env: { command: 'serve', mode: 'development' }, + checkerConfig: entry.checker, + }, + }, + ) + worker.on('message', (message) => messages.push(message)) + worker.on('error', (error) => { + workerError = error + }) + worker.postMessage({ + type: ACTION_TYPES.config, + payload: { enableOverlay: true, enableTerminal: false }, + }) + worker.postMessage({ + type: ACTION_TYPES.configureServer, + payload: { root }, + }) + await expect + .poll(() => messages.length, { timeout: 10000 }) + .toBeGreaterThan(0) + // Prove the watcher is ready via an existing file before creating a new one. + await expect + .poll( + async () => { + await writeFile( + path.join(root, `src/initial.${entry.extension}`), + entry.added, + ) + return JSON.stringify(messages) + }, + { timeout: 5000 }, + ) + .toContain(entry.message) + messages.length = 0 + await writeFile( + path.join(root, `src/added.${entry.extension}`), + entry.added, + ) + await expect + .poll(() => JSON.stringify(messages), { timeout: 5000 }) + .toContain(`src/added.${entry.extension}`) + expect(JSON.stringify(messages)).toContain(entry.message) + expect(workerError).toBeUndefined() + } finally { + await worker?.terminate() + await rm(root, { recursive: true, force: true }) + } +}, 20000) diff --git a/packages/vite-plugin-checker/src/checkers/biome/main.ts b/packages/vite-plugin-checker/src/checkers/biome/main.ts index 15f4d15c..98eaecde 100644 --- a/packages/vite-plugin-checker/src/checkers/biome/main.ts +++ b/packages/vite-plugin-checker/src/checkers/biome/main.ts @@ -140,11 +140,14 @@ const createDiagnostic: CreateDiagnostic<'biome'> = (pluginConfig) => { const watcher = chokidar.watch(watchTarget, { cwd: root, + ignoreInitial: true, ignored: (p: string) => p.includes('node_modules'), }) - watcher.on('change', (filePath) => { + const scheduleLintFile = (filePath: string) => { scheduler.schedule(path.resolve(root, filePath)) - }) + } + watcher.on('add', scheduleLintFile) + watcher.on('change', scheduleLintFile) watcher.on('unlink', (filePath) => { const absPath = path.resolve(root, filePath) manager.updateByFileId(absPath, []) diff --git a/packages/vite-plugin-checker/src/checkers/eslint/main.ts b/packages/vite-plugin-checker/src/checkers/eslint/main.ts index 8ef74959..cd6f783a 100644 --- a/packages/vite-plugin-checker/src/checkers/eslint/main.ts +++ b/packages/vite-plugin-checker/src/checkers/eslint/main.ts @@ -229,17 +229,20 @@ const createDiagnostic: CreateDiagnostic<'eslint'> = (pluginConfig) => { const watcher = chokidar.watch(watchTarget, { cwd: root, + ignoreInitial: true, ignored: createIgnore(root, files), }) - watcher.on('change', (filePath) => { + const scheduleLintFile = (filePath: string) => { const absPath = path.resolve(root, filePath) shouldLintPath(absPath) .then((shouldLint) => { if (shouldLint) scheduler.schedule(absPath) }) .catch(ignoreTransientFsError) - }) + } + watcher.on('add', scheduleLintFile) + watcher.on('change', scheduleLintFile) watcher.on('unlink', (filePath) => { const absPath = path.resolve(root, filePath) manager.updateByFileId(absPath, []) diff --git a/packages/vite-plugin-checker/src/checkers/stylelint/main.ts b/packages/vite-plugin-checker/src/checkers/stylelint/main.ts index 450a6bb6..a8441362 100644 --- a/packages/vite-plugin-checker/src/checkers/stylelint/main.ts +++ b/packages/vite-plugin-checker/src/checkers/stylelint/main.ts @@ -138,12 +138,15 @@ const createDiagnostic: CreateDiagnostic<'stylelint'> = (pluginConfig) => { const watcher = chokidar.watch(watchTarget, { cwd: root, + ignoreInitial: true, ignored: createIgnore(root, translatedOptions.files), }) - watcher.on('change', (filePath) => { + const scheduleLintFile = (filePath: string) => { scheduler.schedule(path.resolve(root, filePath)) - }) + } + watcher.on('add', scheduleLintFile) + watcher.on('change', scheduleLintFile) watcher.on('unlink', (filePath) => { const absPath = path.resolve(root, filePath) manager.updateByFileId(absPath, []) From b3c05a2367ec1b01471b29e85bb6d9075440997b Mon Sep 17 00:00:00 2001 From: Charan Rathore <180254320+charan-rathore@users.noreply.github.com> Date: Thu, 1 Oct 2026 09:37:52 +0530 Subject: [PATCH 2/2] test: normalize diagnostic path separators in new-file checks --- packages/vite-plugin-checker/__tests__/newLintFile.spec.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/vite-plugin-checker/__tests__/newLintFile.spec.ts b/packages/vite-plugin-checker/__tests__/newLintFile.spec.ts index ca09e7cc..29913126 100644 --- a/packages/vite-plugin-checker/__tests__/newLintFile.spec.ts +++ b/packages/vite-plugin-checker/__tests__/newLintFile.spec.ts @@ -105,7 +105,9 @@ it.each( entry.added, ) await expect - .poll(() => JSON.stringify(messages), { timeout: 5000 }) + .poll(() => JSON.stringify(messages).replace(/\\\\/g, '/'), { + timeout: 5000, + }) .toContain(`src/added.${entry.extension}`) expect(JSON.stringify(messages)).toContain(entry.message) expect(workerError).toBeUndefined()