Skip to content

Commit a3565fd

Browse files
novemberbornjamestalmage
authored andcommitted
Improve source pattern handling (#730)
* watcher: correctly test if source is in the cwd Make sure the path starts with ../ (after platform-specific slashes have been corrected). Clarify existing test and add a case where the source starts with two dots but is still in the current working directory. * change handling of negated source patterns Fixes #614. Allow negated source patterns to be specified without unsetting the default negation patterns. Allow source patterns to override the default negation patterns if they start with one of the ignored directories. * update watch mode docs * Suggest `watch:test` as the npm script * Document how to always enable watch mode using the ava section in package.json * Recommend source patterns are configured through the ava section in package.json * Suggest using the verbose reporter when debugging
1 parent 02b0aae commit a3565fd

File tree

3 files changed

+119
-44
lines changed

3 files changed

+119
-44
lines changed

docs/recipes/watch-mode.md

+19-6
Original file line numberDiff line numberDiff line change
@@ -34,15 +34,25 @@ You could also set up a special script:
3434
{
3535
"scripts": {
3636
"test": "ava",
37-
"test:watch": "ava --watch"
37+
"watch:test": "ava --watch"
3838
}
3939
}
4040
```
4141

4242
And then use:
4343

4444
```console
45-
$ npm run test:watch
45+
$ npm run watch:test
46+
```
47+
48+
Finally you could configure AVA to *always* run in watch mode by setting the `watch` key in the [`ava` section of your `package.json`]:
49+
50+
```json
51+
{
52+
"ava": {
53+
"watch": true
54+
}
55+
}
4656
```
4757

4858
Please note that the TAP reporter is unavailable when using watch mode.
@@ -61,7 +71,9 @@ In AVA there's a distinction between *source files* and *test files*. As you can
6171

6272
By default AVA watches for changes to the test files, `package.json`, and any other `.js` files. It'll ignore files in [certain directories](https://github.com/novemberborn/ignore-by-default/blob/master/index.js) as provided by the [`ignore-by-default`] package.
6373

64-
You can configure patterns for the source files using the [`--source` CLI flag] or in the `ava` section of your `package.json` file. Note that if you specify a negative pattern the directories from [`ignore-by-default`] will no longer be ignored, so you may want to repeat these in your config.
74+
You can configure patterns for the source files in the [`ava` section of your `package.json`] file, using the `source` key. This is the recommended way, though you could also use the [`--source` CLI flag].
75+
76+
You can specify patterns to match files in the folders that would otherwise be ignored, e.g. use `node_modules/some-dependency/*.js` to specify all `.js` files in `node_modules/some-dependency` as a source, even though normally all files in `node_modules` are ignored. Note that you need to specify an exact directory; `{bower_components,node_modules}/**/*.js` won't work.
6577

6678
If your tests write to disk they may trigger the watcher to rerun your tests. If this occurs you will need to use the `--source` flag.
6779

@@ -81,17 +93,17 @@ You can quickly rerun all tests by typing <kbd>r</kbd> on the console, followed
8193

8294
## Debugging
8395

84-
Sometimes watch mode does something surprising like rerunning all tests when you thought only a single test would be run. To see its reasoning you can enable a debug mode:
96+
Sometimes watch mode does something surprising like rerunning all tests when you thought only a single test would be run. To see its reasoning you can enable a debug mode. This will work best with the verbose reporter:
8597

8698
```console
87-
$ DEBUG=ava:watcher npm test -- --watch
99+
$ DEBUG=ava:watcher npm test -- --watch --verbose
88100
```
89101

90102
On Windows use:
91103

92104
```console
93105
$ set DEBUG=ava:watcher
94-
$ npm test -- --watch
106+
$ npm test -- --watch --verbose
95107
```
96108

97109
## Help us make watch mode better
@@ -103,3 +115,4 @@ Watch mode is relatively new and there might be some rough edges. Please [report
103115
[`--require` CLI flag]: https://github.com/sindresorhus/ava#cli
104116
[`--source` CLI flag]: https://github.com/sindresorhus/ava#cli
105117
[`.only` modifier]: https://github.com/sindresorhus/ava#running-specific-tests
118+
[`ava` section of your `package.json`]: https://github.com/sindresorhus/ava#configuration

lib/watcher.js

+52-20
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,12 @@ function rethrowAsync(err) {
2727
});
2828
}
2929

30+
function getDefaultIgnorePatterns() {
31+
return defaultIgnore.map(function (dir) {
32+
return dir + '/**/*';
33+
});
34+
}
35+
3036
// Used on paths before they're passed to multimatch to Harmonize matching
3137
// across platforms.
3238
var matchable = process.platform === 'win32' ? slash : function (path) {
@@ -289,52 +295,78 @@ function getChokidarPatterns(files, sources) {
289295
}
290296
});
291297

298+
// Allow source patterns to override the default ignore patterns. Chokidar
299+
// ignores paths that match the list of ignored patterns. It uses anymatch
300+
// under the hood, which supports negation patterns. For any source pattern
301+
// that starts with an ignored directory, ensure the corresponding negation
302+
// pattern is added to the ignored paths.
303+
var overrideDefaultIgnorePatterns = paths.filter(function (pattern) {
304+
return defaultIgnore.indexOf(pattern.split('/')[0]) >= 0;
305+
}).map(function (pattern) {
306+
return '!' + pattern;
307+
});
308+
ignored = getDefaultIgnorePatterns().concat(ignored, overrideDefaultIgnorePatterns);
309+
292310
if (paths.length === 0) {
293311
paths = ['package.json', '**/*.js'];
294312
}
295-
296313
paths = paths.concat(files);
297314

298-
if (ignored.length === 0) {
299-
ignored = defaultIgnore;
300-
}
301-
302315
return {
303316
paths: paths,
304317
ignored: ignored
305318
};
306319
}
307320

308321
function makeSourceMatcher(sources) {
309-
var patterns = sources;
322+
var mixedPatterns = [];
323+
var defaultIgnorePatterns = getDefaultIgnorePatterns();
324+
var overrideDefaultIgnorePatterns = [];
310325

311-
var hasPositivePattern = patterns.some(function (pattern) {
312-
return pattern[0] !== '!';
313-
});
326+
var hasPositivePattern = false;
327+
sources.forEach(function (pattern) {
328+
mixedPatterns.push(pattern);
329+
if (!hasPositivePattern && pattern[0] !== '!') {
330+
hasPositivePattern = true;
331+
}
314332

315-
var hasNegativePattern = patterns.some(function (pattern) {
316-
return pattern[0] === '!';
333+
// Extract patterns that start with an ignored directory. These need to be
334+
// rematched separately.
335+
if (defaultIgnore.indexOf(pattern.split('/')[0]) >= 0) {
336+
overrideDefaultIgnorePatterns.push(pattern);
337+
}
317338
});
318339

319340
// Same defaults as used for Chokidar.
320341
if (!hasPositivePattern) {
321-
patterns = ['package.json', '**/*.js'].concat(patterns);
322-
}
323-
324-
if (!hasNegativePattern) {
325-
patterns = patterns.concat(defaultIgnore.map(function (dir) {
326-
return '!' + dir + '/**/*';
327-
}));
342+
mixedPatterns = ['package.json', '**/*.js'].concat(mixedPatterns);
328343
}
329344

330345
return function (path) {
346+
path = matchable(path);
347+
331348
// Ignore paths outside the current working directory. They can't be matched
332349
// to a pattern.
333-
if (/^\.\./.test(path)) {
350+
if (/^\.\.\//.test(path)) {
351+
return false;
352+
}
353+
354+
var isSource = multimatch(path, mixedPatterns).length === 1;
355+
if (!isSource) {
334356
return false;
335357
}
336358

337-
return multimatch(matchable(path), patterns).length === 1;
359+
var isIgnored = multimatch(path, defaultIgnorePatterns).length === 1;
360+
if (!isIgnored) {
361+
return true;
362+
}
363+
364+
var isErroneouslyIgnored = multimatch(path, overrideDefaultIgnorePatterns).length === 1;
365+
if (isErroneouslyIgnored) {
366+
return true;
367+
}
368+
369+
return false;
338370
};
339371
}
340372

test/watcher.js

+48-18
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,9 @@ group('chokidar is installed', function (beforeEach, test, group) {
174174
t.same(chokidar.watch.firstCall.args, [
175175
['package.json', '**/*.js'].concat(files),
176176
{
177-
ignored: defaultIgnore,
177+
ignored: defaultIgnore.map(function (dir) {
178+
return dir + '/**/*';
179+
}),
178180
ignoreInitial: true
179181
}
180182
]);
@@ -188,21 +190,25 @@ group('chokidar is installed', function (beforeEach, test, group) {
188190
t.same(chokidar.watch.firstCall.args, [
189191
['foo.js', 'baz.js'].concat(files),
190192
{
191-
ignored: ['bar.js', 'qux.js'],
193+
ignored: defaultIgnore.map(function (dir) {
194+
return dir + '/**/*';
195+
}).concat('bar.js', 'qux.js'),
192196
ignoreInitial: true
193197
}
194198
]);
195199
});
196200

197-
test('default set of ignored files if configured sources does not contain exclusion patterns', function (t) {
201+
test('configured sources can override default ignore patterns', function (t) {
198202
t.plan(2);
199-
start(['foo.js', 'baz.js']);
203+
start(['node_modules/foo/*.js']);
200204

201205
t.ok(chokidar.watch.calledOnce);
202206
t.same(chokidar.watch.firstCall.args, [
203-
['foo.js', 'baz.js'].concat(files),
207+
['node_modules/foo/*.js'].concat(files),
204208
{
205-
ignored: defaultIgnore,
209+
ignored: defaultIgnore.map(function (dir) {
210+
return dir + '/**/*';
211+
}).concat('!node_modules/foo/*.js'),
206212
ignoreInitial: true
207213
}
208214
]);
@@ -814,7 +820,7 @@ group('chokidar is installed', function (beforeEach, test, group) {
814820
});
815821
});
816822

817-
test('uses default patterns', function (t) {
823+
test('uses default source patterns', function (t) {
818824
t.plan(4);
819825
seed();
820826

@@ -839,16 +845,11 @@ group('chokidar is installed', function (beforeEach, test, group) {
839845
});
840846
});
841847

842-
test('uses default exclusion patterns if no exclusion pattern is given', function (t) {
848+
test('uses default exclusion patterns', function (t) {
843849
t.plan(2);
844850

845-
// Ensure each directory is treated as containing sources, but rely on
846-
// the default exclusion patterns, also based on these directories, to
847-
// exclude them again.
848-
var sources = defaultIgnore.map(function (dir) {
849-
return dir + '/**/*';
850-
});
851-
seed(sources);
851+
// Ensure each directory is treated as containing sources.
852+
seed(['**/*']);
852853

853854
// Synthesize an excluded file for each directory that's ignored by
854855
// default. Apply deeper nesting for each file.
@@ -857,7 +858,7 @@ group('chokidar is installed', function (beforeEach, test, group) {
857858
for (var i = index; i >= 0; i--) {
858859
relPath = path.join(relPath, String(i));
859860
}
860-
return relPath;
861+
return relPath + '.js';
861862
});
862863

863864
// Ensure test/1.js also depends on the excluded files.
@@ -876,17 +877,46 @@ group('chokidar is installed', function (beforeEach, test, group) {
876877
});
877878
});
878879

879-
test('ignores dependencies outside of the current working directory', function (t) {
880+
test('allows default exclusion patterns to be overriden', function (t) {
880881
t.plan(2);
881-
seed();
882+
seed(['node_modules/foo/*.js']);
883+
884+
var dep = path.join('node_modules', 'foo', 'index.js');
885+
emitDependencies(path.join('test', '1.js'), [path.resolve(dep)]);
886+
change(dep);
887+
888+
return debounce(1).then(function () {
889+
t.ok(api.run.calledTwice);
890+
t.same(api.run.secondCall.args, [[path.join('test', '1.js')], {runOnlyExclusive: false}]);
891+
});
892+
});
893+
894+
test('ignores dependencies outside of the current working directory', function (t) {
895+
t.plan(4);
896+
seed(['**/*.js', '..foo.js']);
882897

883898
emitDependencies(path.join('test', '1.js'), [path.resolve('../outside.js')]);
899+
emitDependencies(path.join('test', '2.js'), [path.resolve('..foo.js')]);
884900
// Pretend Chokidar detected a change to verify (normally Chokidar would
885901
// also be ignoring this file but hey).
886902
change(path.join('..', 'outside.js'));
903+
904+
api.run.returns(Promise.resolve());
887905
return debounce().then(function () {
888906
t.ok(api.run.calledTwice);
907+
// If ../outside.js was tracked as a dependency of test/1.js this would
908+
// have caused test/1.js to be rerun. Instead expect all tests to be
909+
// rerun. This is somewhat artifical: normally changes to ../outside.js
910+
// wouldn't even be picked up. However this lets us test dependency
911+
// tracking without directly inspecting the internal state of the
912+
// watcher.
889913
t.same(api.run.secondCall.args, [files, {runOnlyExclusive: false}]);
914+
915+
change('..foo.js');
916+
return debounce();
917+
}).then(function () {
918+
t.ok(api.run.calledThrice);
919+
t.same(api.run.thirdCall.args, [[path.join('test', '2.js')], {runOnlyExclusive: false}]);
890920
});
891921
});
892922

0 commit comments

Comments
 (0)