Skip to content

Commit 82dbdd5

Browse files
committed
Rewrite watcher
1 parent e3af70a commit 82dbdd5

File tree

12 files changed

+946
-491
lines changed

12 files changed

+946
-491
lines changed

docs/recipes/watch-mode.md

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@ $ npx ava --watch
1414

1515
Please note that integrated debugging and the TAP reporter are unavailable when using watch mode.
1616

17-
## Requirements
17+
## Limitations
1818

19-
AVA uses [`chokidar`] as the file watcher. Note that even if you see warnings about optional dependencies failing during install, it will still work fine. Please refer to the *[Install Troubleshooting]* section of `chokidar` documentation for how to resolve the installation problems with chokidar.
19+
AVA uses `fs.watch()`. On supported platforms, `recursive` mode is used. When not supported, `fs.watch()` is used with individual directories, based on the dependency tracking (see below). This is an approximation. `recursive` mode is available with Node.js 19.1 on Linux, MacOS and Windows so we will not attempt to improve our fallback implementation.
2020

2121
## Ignoring changes
2222

@@ -30,7 +30,7 @@ If your tests write to disk they may trigger the watcher to rerun your tests. Co
3030

3131
AVA tracks which source files your test files depend on. If you change such a dependency only the test file that depends on it will be rerun. AVA will rerun all tests if it cannot determine which test file depends on the changed source file.
3232

33-
Dependency tracking works for required modules. Custom extensions and transpilers are supported, provided you [added them in your `package.json` or `ava.config.*` file][config], and not from inside your test file. Files accessed using the `fs` module are not tracked.
33+
Dependency tracking works for `require()` and `import` syntax, as supported by [@vercel/nft](https://github.com/vercel/nft). `import()` is supported but dynamic paths such as `import(myVariable)` are not. Files accessed using the `fs` module are not tracked.
3434

3535
## Watch mode and the `.only` modifier
3636

@@ -60,8 +60,6 @@ $ DEBUG=ava:watcher npx ava --watch
6060

6161
Watch mode is relatively new and there might be some rough edges. Please [report](https://github.com/avajs/ava/issues) any issues you encounter. Thanks!
6262

63-
[`chokidar`]: https://github.com/paulmillr/chokidar
64-
[Install Troubleshooting]: https://github.com/paulmillr/chokidar#install-troubleshooting
6563
[`ignore-by-default`]: https://github.com/novemberborn/ignore-by-default
6664
[`.only` modifier]: ../01-writing-tests.md#running-specific-tests
6765
[config]: ../06-configuration.md

lib/api.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -200,15 +200,14 @@ export default class Api extends Emittery {
200200

201201
await this.emit('run', {
202202
bailWithoutReporting: debugWithoutSpecificFile,
203-
clearLogOnNextRun: runtimeOptions.clearLogOnNextRun === true,
204203
debug: Boolean(this.options.debug),
205204
failFastEnabled: failFast,
206205
filePathPrefix: getFilePathPrefix(selectedFiles),
207206
files: selectedFiles,
208207
matching: apiOptions.match.length > 0,
209208
previousFailures: runtimeOptions.previousFailures || 0,
210209
runOnlyExclusive: runtimeOptions.runOnlyExclusive === true,
211-
runVector: runtimeOptions.runVector || 0,
210+
firstRun: runtimeOptions.firstRun ?? true,
212211
status: runStatus,
213212
});
214213

@@ -306,7 +305,8 @@ export default class Api extends Emittery {
306305

307306
// Allow shared workers to clean up before the run ends.
308307
await Promise.all(deregisteredSharedWorkers);
309-
scheduler.storeFailedTestFiles(runStatus, this.options.cacheEnabled === false ? null : this._createCacheDir());
308+
const files = scheduler.storeFailedTestFiles(runStatus, this.options.cacheEnabled === false ? null : this._createCacheDir());
309+
runStatus.emitStateChange({type: 'touched-files', files});
310310
} catch (error) {
311311
if (error && error.name === 'AggregateError') {
312312
for (const error_ of error.errors) {

lib/cli.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import pkg from './pkg.cjs';
2525
import providerManager from './provider-manager.js';
2626
import DefaultReporter from './reporters/default.js';
2727
import TapReporter from './reporters/tap.js';
28-
import Watcher from './watcher.js';
28+
import {start as startWatcher} from './watcher.js';
2929

3030
function exit(message) {
3131
console.error(`\n ${chalk.red(figures.cross)} ${message}`);
@@ -469,15 +469,15 @@ export default async function loadCli() { // eslint-disable-line complexity
469469
});
470470

471471
if (combined.watch) {
472-
const watcher = new Watcher({
472+
startWatcher({
473473
api,
474474
filter,
475475
globs,
476476
projectDir,
477477
providers,
478478
reporter,
479+
stdin: process.stdin,
479480
});
480-
watcher.observeStdin(process.stdin);
481481
} else {
482482
let debugWithoutSpecificFile = false;
483483
api.on('run', plan => {

lib/glob-helpers.cjs

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,6 @@ const defaultPicomatchIgnorePatterns = [
1515
...defaultIgnorePatterns.map(pattern => `${pattern}/**/*`),
1616
];
1717

18-
const defaultMatchNoIgnore = picomatch(defaultPicomatchIgnorePatterns);
19-
2018
const matchingCache = new WeakMap();
2119
const processMatchingPatterns = input => {
2220
let result = matchingCache.get(input);
@@ -45,15 +43,9 @@ const processMatchingPatterns = input => {
4543

4644
exports.processMatchingPatterns = processMatchingPatterns;
4745

48-
const matchesIgnorePatterns = (file, patterns) => {
49-
const {matchNoIgnore} = processMatchingPatterns(patterns);
50-
return matchNoIgnore(file) || defaultMatchNoIgnore(file);
51-
};
52-
53-
function classify(file, {cwd, extensions, filePatterns, ignoredByWatcherPatterns}) {
46+
function classify(file, {cwd, extensions, filePatterns}) {
5447
file = normalizeFileForMatching(cwd, file);
5548
return {
56-
isIgnoredByWatcher: matchesIgnorePatterns(file, ignoredByWatcherPatterns),
5749
isTest: hasExtension(extensions, file) && !isHelperish(file) && filePatterns.length > 0 && matches(file, filePatterns),
5850
};
5951
}

lib/globs.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import fs from 'node:fs';
22
import path from 'node:path';
33

44
import {globby, globbySync} from 'globby';
5+
import picomatch from 'picomatch';
56

67
import {
78
defaultIgnorePatterns,
@@ -126,11 +127,13 @@ export async function findTests({cwd, extensions, filePatterns}) {
126127
return files.filter(file => !path.basename(file).startsWith('_'));
127128
}
128129

129-
export function getChokidarIgnorePatterns({ignoredByWatcherPatterns}) {
130-
return [
130+
export function buildIgnoreMatcher({ignoredByWatcherPatterns}) {
131+
const patterns = [
131132
...defaultIgnorePatterns.map(pattern => `${pattern}/**/*`),
132133
...ignoredByWatcherPatterns.filter(pattern => !pattern.startsWith('!')),
133134
];
135+
136+
return picomatch(patterns, {dot: true});
134137
}
135138

136139
export function applyTestFileFilter({ // eslint-disable-line complexity

lib/reporters/default.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ export default class Reporter {
148148
this.consumeStateChange(evt);
149149
});
150150

151-
if (this.watching && plan.runVector > 1) {
151+
if (this.watching && !plan.firstRun) {
152152
this.lineWriter.write(chalk.gray.dim('\u2500'.repeat(this.lineWriter.columns)) + os.EOL);
153153
}
154154

lib/scheduler.js

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,22 @@ const scheduler = {
1313
return;
1414
}
1515

16+
const filename = path.join(cacheDir, FILENAME);
17+
// Given that we're writing to a cache directory, consider this file
18+
// temporary.
19+
const temporaryFiles = [filename];
1620
try {
17-
writeFileAtomic.sync(path.join(cacheDir, FILENAME), JSON.stringify(runStatus.getFailedTestFiles()));
21+
writeFileAtomic.sync(filename, JSON.stringify(runStatus.getFailedTestFiles()), {
22+
tmpfileCreated(tmpfile) {
23+
temporaryFiles.push(tmpfile);
24+
},
25+
});
1826
} catch {}
27+
28+
return {
29+
changedFiles: [],
30+
temporaryFiles,
31+
};
1932
},
2033

2134
// Order test-files, so that files with failing tests come first

0 commit comments

Comments
 (0)