Skip to content

Commit 426ac22

Browse files
committed
Add a brand new watcher
Rely on recursive fs.watch(), rather than Chokidar. On Linux this is supported from Node.js 20 onwards. It won't work for network shares and Docker volume mounts which would require polling, we'll find out if that's a problem or not. (For now, the previous implementation is still available.) Use @vercel/nft to perform static dependency analysis, supporting ESM and CJS imports for JavaScript & TypeScript source files. This is a huge improvement over the previous runtime tracking of CJS imports, which did not support ESM. Rewrite the change handling logic to be easier to follow (though it's still pretty complicated). Improve integration with `@ava/typescript`. The watcher can now detect a change to a TypeScript source file, then wait for the corresponding build output to change before re-running tests.
1 parent 344ff33 commit 426ac22

File tree

63 files changed

+1675
-132
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

63 files changed

+1675
-132
lines changed

docs/recipes/watch-mode.md

+4
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ AVA 5 uses [`chokidar`] as the file watcher. Note that even if you see warnings
2929

3030
The same applies with AVA 6 when using the `ava5+chokidar` watcher. However you'll need to install `chokidar` separately.
3131

32+
Otherwise, AVA 6 uses `fs.watch()`. Support for `recursive` mode is required. Note that this has only become available on Linux since Node.js 20. [Other caveats apply](https://nodejs.org/api/fs.html#caveats), for example this won't work well on network filesystems and Docker host mounts.
33+
3234
## Ignoring changes
3335

3436
By default AVA watches for changes to all files, except for those with a `.snap.md` extension, `ava.config.*` and files in [certain directories](https://github.com/novemberborn/ignore-by-default/blob/master/index.js) as provided by the [`ignore-by-default`] package.
@@ -43,6 +45,8 @@ AVA tracks which source files your test files depend on. If you change such a de
4345

4446
AVA 5 (and the `ava5+chokidar` watcher in AVA 6) spies on `require()` calls to track dependencies. 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.
4547

48+
With AVA 6, 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.
49+
4650
Files accessed using the `fs` module are not tracked.
4751

4852
## Watch mode and the `.only` modifier

lib/api.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -315,7 +315,7 @@ export default class Api extends Emittery {
315315
}
316316

317317
timeoutTrigger.discard();
318-
return runStatus;
318+
return runStatus.end();
319319
}
320320

321321
_getLocalCacheDir() {

lib/ava5-watcher.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import chokidar from 'chokidar';
44
import createDebug from 'debug';
55

66
import {chalk} from './chalk.js';
7-
import {applyTestFileFilter, classify, getChokidarIgnorePatterns} from './globs.js';
7+
import {applyTestFileFilter, classifyAva5Watcher as classify, getChokidarIgnorePatterns} from './globs.js';
88

99
const debug = createDebug('ava:watcher');
1010

lib/cli.js

+26-2
Original file line numberDiff line numberDiff line change
@@ -453,7 +453,7 @@ export default async function loadCli() { // eslint-disable-line complexity
453453
api.on('run', plan => {
454454
reporter.startRun(plan);
455455

456-
if (process.env.AVA_EMIT_RUN_STATUS_OVER_IPC === 'I\'ll find a payphone baby / Take some time to talk to you') {
456+
if (process.env.TEST_AVA) {
457457
const bufferedSend = controlFlow(process);
458458

459459
plan.status.on('stateChange', evt => {
@@ -492,7 +492,31 @@ export default async function loadCli() { // eslint-disable-line complexity
492492
exit('The "watcher" option must be set to "ava5+chokidar"');
493493
}
494494
} else {
495-
exit('TODO');
495+
const {available, start} = await import('./watcher.js');
496+
if (!available(projectDir)) {
497+
exit('Watch mode requires support for recursive fs.watch()');
498+
return;
499+
}
500+
501+
const abortController = new AbortController();
502+
process.on('message', message => {
503+
if (message === 'abort-watcher') {
504+
abortController.abort();
505+
v8.takeCoverage();
506+
}
507+
});
508+
process.channel?.unref();
509+
510+
start({
511+
api,
512+
filter,
513+
globs,
514+
projectDir,
515+
providers,
516+
reporter,
517+
stdin: process.stdin,
518+
signal: abortController.signal,
519+
});
496520
}
497521
} else {
498522
let debugWithoutSpecificFile = false;

lib/glob-helpers.cjs

+12-2
Original file line numberDiff line numberDiff line change
@@ -46,20 +46,30 @@ const processMatchingPatterns = input => {
4646

4747
exports.processMatchingPatterns = processMatchingPatterns;
4848

49+
function classify(file, {cwd, extensions, filePatterns}) {
50+
file = normalizeFileForMatching(cwd, file);
51+
return {
52+
isTest: hasExtension(extensions, file) && !isHelperish(file) && filePatterns.length > 0 && matches(file, filePatterns),
53+
};
54+
}
55+
56+
exports.classify = classify;
57+
4958
const matchesIgnorePatterns = (file, patterns) => {
5059
const {matchNoIgnore} = processMatchingPatterns(patterns);
5160
return matchNoIgnore(file) || defaultMatchNoIgnore(file);
5261
};
5362

54-
function classify(file, {cwd, extensions, filePatterns, ignoredByWatcherPatterns}) {
63+
function classifyAva5Watcher(file, {cwd, extensions, filePatterns, ignoredByWatcherPatterns}) {
5564
file = normalizeFileForMatching(cwd, file);
5665
return {
5766
isIgnoredByWatcher: matchesIgnorePatterns(file, ignoredByWatcherPatterns),
5867
isTest: hasExtension(extensions, file) && !isHelperish(file) && filePatterns.length > 0 && matches(file, filePatterns),
5968
};
6069
}
6170

62-
exports.classify = classify;
71+
// TODO: Delete along with ava5+chokidar watcher.
72+
exports.classifyAva5Watcher = classifyAva5Watcher;
6373

6474
const hasExtension = (extensions, file) => extensions.includes(path.extname(file).slice(1));
6575

lib/globs.js

+12
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,
@@ -13,6 +14,7 @@ import {
1314

1415
export {
1516
classify,
17+
classifyAva5Watcher,
1618
isHelperish,
1719
matches,
1820
normalizePattern,
@@ -126,13 +128,23 @@ export async function findTests({cwd, extensions, filePatterns}) {
126128
return files.filter(file => !path.basename(file).startsWith('_'));
127129
}
128130

131+
// TODO: Delete along with ava5+chokidar watcher.
129132
export function getChokidarIgnorePatterns({ignoredByWatcherPatterns}) {
130133
return [
131134
...defaultIgnorePatterns.map(pattern => `${pattern}/**/*`),
132135
...ignoredByWatcherPatterns.filter(pattern => !pattern.startsWith('!')),
133136
];
134137
}
135138

139+
export function buildIgnoreMatcher({ignoredByWatcherPatterns}) {
140+
const patterns = [
141+
...defaultIgnorePatterns.map(pattern => `${pattern}/**/*`),
142+
...ignoredByWatcherPatterns.filter(pattern => !pattern.startsWith('!')),
143+
];
144+
145+
return picomatch(patterns, {dot: true});
146+
}
147+
136148
export function applyTestFileFilter({ // eslint-disable-line complexity
137149
cwd,
138150
expandDirectories = true,

lib/provider-manager.js

+8-9
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
import * as globs from './globs.js';
22
import pkg from './pkg.cjs';
33

4-
const levels = {
4+
export const levels = {
55
// As the protocol changes, comparing levels by integer allows AVA to be
6-
// compatible with different versions. Currently there is only one supported
7-
// version, so this is effectively unused. The infrastructure is retained for
8-
// future use.
9-
levelIntegersAreCurrentlyUnused: 0,
6+
// compatible with different versions.
7+
ava3Stable: 1,
8+
ava6: 2,
109
};
1110

12-
const levelsByProtocol = {
13-
'ava-3.2': levels.levelIntegersAreCurrentlyUnused,
14-
};
11+
const levelsByProtocol = Object.assign(Object.create(null), {
12+
'ava-3.2': levels.ava3Stable,
13+
'ava-6': levels.ava6,
14+
});
1515

1616
async function load(providerModule, projectDir) {
1717
const ava = {version: pkg.version};
@@ -50,7 +50,6 @@ async function load(providerModule, projectDir) {
5050
}
5151

5252
const providerManager = {
53-
levels,
5453
async typescript(projectDir) {
5554
return load('@ava/typescript', projectDir);
5655
},

lib/run-status.js

+5
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,11 @@ export default class RunStatus extends Emittery {
209209
this.emit('stateChange', event);
210210
}
211211

212+
end() {
213+
this.emitStateChange({type: 'end'});
214+
return this;
215+
}
216+
212217
suggestExitCode(circumstances) {
213218
if (this.emptyParallelRun) {
214219
return 0;

0 commit comments

Comments
 (0)