Skip to content

Commit

Permalink
Add a brand new watcher
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
novemberborn committed Jun 29, 2023
1 parent 344ff33 commit 426ac22
Show file tree
Hide file tree
Showing 63 changed files with 1,675 additions and 132 deletions.
4 changes: 4 additions & 0 deletions docs/recipes/watch-mode.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ AVA 5 uses [`chokidar`] as the file watcher. Note that even if you see warnings

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

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.

## Ignoring changes

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.
Expand All @@ -43,6 +45,8 @@ AVA tracks which source files your test files depend on. If you change such a de

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.

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.

Files accessed using the `fs` module are not tracked.

## Watch mode and the `.only` modifier
Expand Down
2 changes: 1 addition & 1 deletion lib/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,7 @@ export default class Api extends Emittery {
}

timeoutTrigger.discard();
return runStatus;
return runStatus.end();
}

_getLocalCacheDir() {
Expand Down
2 changes: 1 addition & 1 deletion lib/ava5-watcher.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import chokidar from 'chokidar';
import createDebug from 'debug';

import {chalk} from './chalk.js';
import {applyTestFileFilter, classify, getChokidarIgnorePatterns} from './globs.js';
import {applyTestFileFilter, classifyAva5Watcher as classify, getChokidarIgnorePatterns} from './globs.js';

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

Expand Down
28 changes: 26 additions & 2 deletions lib/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -453,7 +453,7 @@ export default async function loadCli() { // eslint-disable-line complexity
api.on('run', plan => {
reporter.startRun(plan);

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

plan.status.on('stateChange', evt => {
Expand Down Expand Up @@ -492,7 +492,31 @@ export default async function loadCli() { // eslint-disable-line complexity
exit('The "watcher" option must be set to "ava5+chokidar"');
}

Check warning on line 493 in lib/cli.js

View check run for this annotation

Codecov / codecov/patch

lib/cli.js#L480-L493

Added lines #L480 - L493 were not covered by tests
} else {
exit('TODO');
const {available, start} = await import('./watcher.js');
if (!available(projectDir)) {
exit('Watch mode requires support for recursive fs.watch()');
return;
}

const abortController = new AbortController();
process.on('message', message => {
if (message === 'abort-watcher') {
abortController.abort();
v8.takeCoverage();
}
});
process.channel?.unref();

start({
api,
filter,
globs,
projectDir,
providers,
reporter,
stdin: process.stdin,
signal: abortController.signal,
});
}
} else {
let debugWithoutSpecificFile = false;
Expand Down
14 changes: 12 additions & 2 deletions lib/glob-helpers.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -46,20 +46,30 @@ const processMatchingPatterns = input => {

exports.processMatchingPatterns = processMatchingPatterns;

function classify(file, {cwd, extensions, filePatterns}) {
file = normalizeFileForMatching(cwd, file);
return {
isTest: hasExtension(extensions, file) && !isHelperish(file) && filePatterns.length > 0 && matches(file, filePatterns),
};
}

exports.classify = classify;

const matchesIgnorePatterns = (file, patterns) => {
const {matchNoIgnore} = processMatchingPatterns(patterns);
return matchNoIgnore(file) || defaultMatchNoIgnore(file);
};

function classify(file, {cwd, extensions, filePatterns, ignoredByWatcherPatterns}) {
function classifyAva5Watcher(file, {cwd, extensions, filePatterns, ignoredByWatcherPatterns}) {
file = normalizeFileForMatching(cwd, file);
return {
isIgnoredByWatcher: matchesIgnorePatterns(file, ignoredByWatcherPatterns),
isTest: hasExtension(extensions, file) && !isHelperish(file) && filePatterns.length > 0 && matches(file, filePatterns),
};
}

exports.classify = classify;
// TODO: Delete along with ava5+chokidar watcher.
exports.classifyAva5Watcher = classifyAva5Watcher;

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

Expand Down
12 changes: 12 additions & 0 deletions lib/globs.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import fs from 'node:fs';
import path from 'node:path';

import {globby, globbySync} from 'globby';
import picomatch from 'picomatch';

import {
defaultIgnorePatterns,
Expand All @@ -13,6 +14,7 @@ import {

export {
classify,
classifyAva5Watcher,
isHelperish,
matches,
normalizePattern,
Expand Down Expand Up @@ -126,13 +128,23 @@ export async function findTests({cwd, extensions, filePatterns}) {
return files.filter(file => !path.basename(file).startsWith('_'));
}

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

export function buildIgnoreMatcher({ignoredByWatcherPatterns}) {
const patterns = [
...defaultIgnorePatterns.map(pattern => `${pattern}/**/*`),
...ignoredByWatcherPatterns.filter(pattern => !pattern.startsWith('!')),
];

return picomatch(patterns, {dot: true});
}

export function applyTestFileFilter({ // eslint-disable-line complexity
cwd,
expandDirectories = true,
Expand Down
17 changes: 8 additions & 9 deletions lib/provider-manager.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import * as globs from './globs.js';
import pkg from './pkg.cjs';

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

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

async function load(providerModule, projectDir) {
const ava = {version: pkg.version};
Expand Down Expand Up @@ -50,7 +50,6 @@ async function load(providerModule, projectDir) {
}

const providerManager = {
levels,
async typescript(projectDir) {
return load('@ava/typescript', projectDir);
},
Expand Down
5 changes: 5 additions & 0 deletions lib/run-status.js
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,11 @@ export default class RunStatus extends Emittery {
this.emit('stateChange', event);
}

end() {
this.emitStateChange({type: 'end'});
return this;
}

suggestExitCode(circumstances) {
if (this.emptyParallelRun) {
return 0;
Expand Down

0 comments on commit 426ac22

Please sign in to comment.