diff --git a/package.json b/package.json index db509a616..228e43453 100644 --- a/package.json +++ b/package.json @@ -65,6 +65,7 @@ "fast-glob": "^3.2.7", "find-up": "^6.2.0", "log-update": "^5.0.0", + "micromatch": "^4.0.4", "nanoid": "^3.1.30", "picocolors": "^1.0.0", "piscina": "^3.2.0", @@ -82,6 +83,7 @@ "@types/chai-subset": "^1.3.3", "@types/diff": "^5.0.1", "@types/jsdom": "^16.2.13", + "@types/micromatch": "^4.0.2", "@types/natural-compare": "^1.4.1", "@types/node": "^16.11.12", "@types/sade": "^1.7.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bd8cef6a5..59eca2999 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,7 @@ importers: '@types/chai-subset': ^1.3.3 '@types/diff': ^5.0.1 '@types/jsdom': ^16.2.13 + '@types/micromatch': ^4.0.2 '@types/natural-compare': ^1.4.1 '@types/node': ^16.11.12 '@types/sade': ^1.7.3 @@ -30,6 +31,7 @@ importers: happy-dom: ^2.24.5 jsdom: ^19.0.0 log-update: ^5.0.0 + micromatch: ^4.0.4 nanoid: ^3.1.30 npm-run-all: ^4.1.5 picocolors: ^1.0.0 @@ -56,6 +58,7 @@ importers: fast-glob: 3.2.7 find-up: 6.2.0 log-update: 5.0.0 + micromatch: 4.0.4 nanoid: 3.1.30 picocolors: 1.0.0 piscina: 3.2.0 @@ -72,6 +75,7 @@ importers: '@types/chai-subset': 1.3.3 '@types/diff': 5.0.1 '@types/jsdom': 16.2.13 + '@types/micromatch': 4.0.2 '@types/natural-compare': 1.4.1 '@types/node': 16.11.12 '@types/sade': 1.7.3 @@ -991,6 +995,10 @@ packages: resolution: {integrity: sha512-HnYpAE1Y6kRyKM/XkEuiRQhTHvkzMBurTHnpFLYLBGPIylZNPs9jJcuOOYWxPLJCSEtmZT0Y8rHDokKN7rRTig==} dev: true + /@types/braces/3.0.1: + resolution: {integrity: sha512-+euflG6ygo4bn0JHtn4pYqcXwRtLvElQ7/nnjDu7iYG56H0+OhCd7d6Ug0IE3WcFpZozBKW2+80FUbv5QGk5AQ==} + dev: true + /@types/chai-subset/1.3.3: resolution: {integrity: sha512-frBecisrNGz+F4T6bcc+NLeolfiojh5FxW2klu669+8BARtyQv2C/GkNW6FUodVe4BroGMP/wER/YDGc7rEllw==} dependencies: @@ -1045,6 +1053,12 @@ packages: resolution: {integrity: sha1-7ihweulOEdK4J7y+UnC86n8+ce4=} dev: true + /@types/micromatch/4.0.2: + resolution: {integrity: sha512-oqXqVb0ci19GtH0vOA/U2TmHTcRY9kuZl4mqUxe0QmJAlIW13kzhuK5pi1i9+ngav8FjpSb9FVS/GE00GLX1VA==} + dependencies: + '@types/braces': 3.0.1 + dev: true + /@types/mri/1.1.1: resolution: {integrity: sha512-nJOuiTlsvmClSr3+a/trTSx4DTuY/VURsWGKSf/eeavh0LRMqdsK60ti0TlwM5iHiGOK3/Ibkxsbr7i9rzGreA==} dev: true diff --git a/src/node/glob.ts b/src/node/glob.ts index c4f87ea4c..7dac91b8c 100644 --- a/src/node/glob.ts +++ b/src/node/glob.ts @@ -1,14 +1,20 @@ import fg from 'fast-glob' +import mm from 'micromatch' import type { ResolvedConfig } from '../types' -import { defaultIncludes, defaultExcludes } from '../constants' + +export function isTargetFile(id: string, config: ResolvedConfig): boolean { + if (mm.isMatch(id, config.excludes)) + return false + return mm.isMatch(id, config.includes) +} export async function globTestFiles(config: ResolvedConfig) { let testFilepaths = await fg( - config.includes || defaultIncludes, + config.includes, { absolute: true, cwd: config.root, - ignore: config.excludes || defaultExcludes, + ignore: config.excludes, }, ) diff --git a/src/node/init.ts b/src/node/init.ts index 1875d4b48..f8e2eabb3 100644 --- a/src/node/init.ts +++ b/src/node/init.ts @@ -3,6 +3,7 @@ import { findUp } from 'find-up' import { createServer } from 'vite' import { toArray } from '@antfu/utils' import type { CliOptions, ResolvedConfig } from '../types' +import { defaultExcludes, defaultIncludes } from '../constants' const configFiles = [ 'vitest.config.ts', @@ -55,6 +56,9 @@ export async function initViteServer(options: CliOptions = {}) { resolved.threads = resolved.threads ?? true resolved.interpretDefault = resolved.interpretDefault ?? true + resolved.includes = resolved.includes ?? defaultIncludes + resolved.excludes = resolved.excludes ?? defaultExcludes + const CI = !!process.env.CI const UPDATE_SNAPSHOT = resolved.update || process.env.UPDATE_SNAPSHOT resolved.snapshotOptions = { diff --git a/src/node/watcher.ts b/src/node/watcher.ts index e6bd67bc6..1f1032440 100644 --- a/src/node/watcher.ts +++ b/src/node/watcher.ts @@ -1,7 +1,10 @@ import { slash } from '@antfu/utils' import type { VitestContext } from '../types' +import { isTargetFile } from './glob' import type { WorkerPool } from './pool' +const WATCHER_DEBOUNCE = 50 + export async function startWatcher(ctx: VitestContext, pool: WorkerPool) { const { reporter, server } = ctx reporter.onWatcherStart?.() @@ -11,16 +14,31 @@ export async function startWatcher(ctx: VitestContext, pool: WorkerPool) { const changedTests = new Set() const seen = new Set() - // TODO: on('add') hook and glob to detect newly added files - server.watcher.on('change', async(id) => { + server.watcher.on('change', (id) => { id = slash(id) - getAffectedTests(ctx, id, changedTests, seen) - if (changedTests.size === 0) return + rerunFile(id) + }) + server.watcher.on('unlink', (id) => { + id = slash(id) + seen.add(id) - // debounce + if (id in ctx.state.filesMap) { + delete ctx.state.filesMap[id] + changedTests.delete(id) + } + }) + server.watcher.on('add', async(id) => { + id = slash(id) + if (isTargetFile(id, ctx.config)) { + changedTests.add(id) + rerunFile(id) + } + }) + + function rerunFile(id: string) { clearTimeout(timer) timer = setTimeout(async() => { if (changedTests.size === 0) { @@ -45,8 +63,8 @@ export async function startWatcher(ctx: VitestContext, pool: WorkerPool) { await reporter.onFinished?.(ctx.state.getFiles(tests)) await reporter.onWatcherStart?.() - }, 100) - }) + }, WATCHER_DEBOUNCE) + } // add an empty promise so it never resolves await new Promise(() => { })