Skip to content

Commit

Permalink
refactor watcher (#766)
Browse files Browse the repository at this point in the history
* refactor watcher tests

* move some methods from watcher to AvaFiles

* add some tests for ava-files

* add more test coverage

* fix linter error after merge

* PR Feedback

* use cross-platform split
  • Loading branch information
jamestalmage committed May 20, 2016
1 parent 54e1106 commit fdd7970
Show file tree
Hide file tree
Showing 4 changed files with 327 additions and 225 deletions.
154 changes: 153 additions & 1 deletion lib/ava-files.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ var Promise = require('bluebird');
var slash = require('slash');
var globby = require('globby');
var flatten = require('arr-flatten');
var defaultIgnore = require('ignore-by-default').directories();
var multimatch = require('multimatch');

function defaultExcludePatterns() {
return [
Expand All @@ -23,7 +25,7 @@ function defaultIncludePatterns() {
];
}

function AvaFiles(files) {
function AvaFiles(files, sources) {
if (!(this instanceof AvaFiles)) {
throw new TypeError('Class constructor AvaFiles cannot be invoked without \'new\'');
}
Expand All @@ -35,12 +37,162 @@ function AvaFiles(files) {
this.excludePatterns = defaultExcludePatterns();

this.files = files;
this.sources = sources || [];
}

AvaFiles.prototype.findTestFiles = function () {
return handlePaths(this.files, this.excludePatterns);
};

function getDefaultIgnorePatterns() {
return defaultIgnore.map(function (dir) {
return dir + '/**/*';
});
}

// Used on paths before they're passed to multimatch to harmonize matching
// across platforms.
var matchable = process.platform === 'win32' ? slash : function (path) {
return path;
};

AvaFiles.prototype.makeSourceMatcher = function () {
var mixedPatterns = [];
var defaultIgnorePatterns = getDefaultIgnorePatterns();
var overrideDefaultIgnorePatterns = [];

var hasPositivePattern = false;
this.sources.forEach(function (pattern) {
mixedPatterns.push(pattern);
// TODO: why not just pattern[0] !== '!'
if (!hasPositivePattern && pattern[0] !== '!') {
hasPositivePattern = true;
}

// Extract patterns that start with an ignored directory. These need to be
// rematched separately.
if (defaultIgnore.indexOf(pattern.split('/')[0]) >= 0) {
overrideDefaultIgnorePatterns.push(pattern);
}
});

// Same defaults as used for Chokidar.
if (!hasPositivePattern) {
mixedPatterns = ['package.json', '**/*.js'].concat(mixedPatterns);
}

return function (path) {
path = matchable(path);

// Ignore paths outside the current working directory. They can't be matched
// to a pattern.
if (/^\.\.\//.test(path)) {
return false;
}

var isSource = multimatch(path, mixedPatterns).length === 1;
if (!isSource) {
return false;
}

var isIgnored = multimatch(path, defaultIgnorePatterns).length === 1;
if (!isIgnored) {
return true;
}

var isErroneouslyIgnored = multimatch(path, overrideDefaultIgnorePatterns).length === 1;
if (isErroneouslyIgnored) {
return true;
}

return false;
};
};

AvaFiles.prototype.makeTestMatcher = function () {
var excludePatterns = this.excludePatterns;
var initialPatterns = this.files.concat(excludePatterns);

return function (filepath) {
// Like in api.js, tests must be .js files and not start with _
if (path.extname(filepath) !== '.js' || path.basename(filepath)[0] === '_') {
return false;
}

// Check if the entire path matches a pattern.
if (multimatch(matchable(filepath), initialPatterns).length === 1) {
return true;
}

// Check if the path contains any directory components.
var dirname = path.dirname(filepath);
if (dirname === '.') {
return false;
}

// Compute all possible subpaths. Note that the dirname is assumed to be
// relative to the working directory, without a leading `./`.
var subpaths = dirname.split(/[\\\/]/).reduce(function (subpaths, component) {
var parent = subpaths[subpaths.length - 1];
if (parent) {
// Always use / to makes multimatch consistent across platforms.
subpaths.push(parent + '/' + component);
} else {
subpaths.push(component);
}
return subpaths;
}, []);

// Check if any of the possible subpaths match a pattern. If so, generate a
// new pattern with **/*.js.
var recursivePatterns = subpaths.filter(function (subpath) {
return multimatch(subpath, initialPatterns).length === 1;
}).map(function (subpath) {
// Always use / to makes multimatch consistent across platforms.
return subpath + '/**/*.js';
});

// See if the entire path matches any of the subpaths patterns, taking the
// excludePatterns into account. This mimicks the behavior in api.js
return multimatch(matchable(filepath), recursivePatterns.concat(excludePatterns)).length === 1;
};
};

AvaFiles.prototype.getChokidarPatterns = function () {
var paths = [];
var ignored = [];

this.sources.forEach(function (pattern) {
if (pattern[0] === '!') {
ignored.push(pattern.slice(1));
} else {
paths.push(pattern);
}
});

// Allow source patterns to override the default ignore patterns. Chokidar
// ignores paths that match the list of ignored patterns. It uses anymatch
// under the hood, which supports negation patterns. For any source pattern
// that starts with an ignored directory, ensure the corresponding negation
// pattern is added to the ignored paths.
var overrideDefaultIgnorePatterns = paths.filter(function (pattern) {
return defaultIgnore.indexOf(pattern.split('/')[0]) >= 0;
}).map(function (pattern) {
return '!' + pattern;
});
ignored = getDefaultIgnorePatterns().concat(ignored, overrideDefaultIgnorePatterns);

if (paths.length === 0) {
paths = ['package.json', '**/*.js'];
}
paths = paths.concat(this.files);

return {
paths: paths,
ignored: ignored
};
};

function handlePaths(files, excludePatterns) {
// convert pinkie-promise to Bluebird promise
files = Promise.resolve(globby(files.concat(excludePatterns)));
Expand Down
164 changes: 8 additions & 156 deletions lib/watcher.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,6 @@ var diff = require('arr-diff');
var flatten = require('arr-flatten');
var union = require('array-union');
var uniq = require('array-uniq');
var defaultIgnore = require('ignore-by-default').directories();
var multimatch = require('multimatch');
var slash = require('slash');
var AvaError = require('./ava-error');
var AvaFiles = require('./ava-files');

Expand All @@ -27,22 +24,11 @@ function rethrowAsync(err) {
});
}

function getDefaultIgnorePatterns() {
return defaultIgnore.map(function (dir) {
return dir + '/**/*';
});
}

// Used on paths before they're passed to multimatch to Harmonize matching
// across platforms.
var matchable = process.platform === 'win32' ? slash : function (path) {
return path;
};

function Watcher(logger, api, files, sources) {
this.debouncer = new Debouncer(this);
this.avaFiles = new AvaFiles(files, sources);

this.isTest = makeTestMatcher(files, AvaFiles.defaultExcludePatterns());
this.isTest = this.avaFiles.makeTestMatcher();

this.clearLogOnNextRun = true;
this.runVector = 0;
Expand Down Expand Up @@ -100,15 +86,15 @@ function Watcher(logger, api, files, sources) {
this.trackFailures(api);

this.dirtyStates = {};
this.watchFiles(files, sources);
this.watchFiles();
this.rerunAll();
}

module.exports = Watcher;

Watcher.prototype.watchFiles = function (files, sources) {
Watcher.prototype.watchFiles = function () {
var self = this;
var patterns = getChokidarPatterns(files, sources);
var patterns = this.avaFiles.getChokidarPatterns();

requireChokidar().watch(patterns.paths, {
ignored: patterns.ignored,
Expand All @@ -122,9 +108,10 @@ Watcher.prototype.watchFiles = function (files, sources) {
});
};

Watcher.prototype.trackTestDependencies = function (api, sources) {
Watcher.prototype.trackTestDependencies = function (api) {
var self = this;
var isSource = makeSourceMatcher(sources);
var isSource = this.avaFiles.makeSourceMatcher();

var relative = function (absPath) {
return nodePath.relative('.', absPath);
};
Expand Down Expand Up @@ -365,141 +352,6 @@ Debouncer.prototype.cancel = function () {
}
};

function getChokidarPatterns(files, sources) {
var paths = [];
var ignored = [];

sources.forEach(function (pattern) {
if (pattern[0] === '!') {
ignored.push(pattern.slice(1));
} else {
paths.push(pattern);
}
});

// Allow source patterns to override the default ignore patterns. Chokidar
// ignores paths that match the list of ignored patterns. It uses anymatch
// under the hood, which supports negation patterns. For any source pattern
// that starts with an ignored directory, ensure the corresponding negation
// pattern is added to the ignored paths.
var overrideDefaultIgnorePatterns = paths.filter(function (pattern) {
return defaultIgnore.indexOf(pattern.split('/')[0]) >= 0;
}).map(function (pattern) {
return '!' + pattern;
});
ignored = getDefaultIgnorePatterns().concat(ignored, overrideDefaultIgnorePatterns);

if (paths.length === 0) {
paths = ['package.json', '**/*.js'];
}
paths = paths.concat(files);

return {
paths: paths,
ignored: ignored
};
}

function makeSourceMatcher(sources) {
var mixedPatterns = [];
var defaultIgnorePatterns = getDefaultIgnorePatterns();
var overrideDefaultIgnorePatterns = [];

var hasPositivePattern = false;
sources.forEach(function (pattern) {
mixedPatterns.push(pattern);
if (!hasPositivePattern && pattern[0] !== '!') {
hasPositivePattern = true;
}

// Extract patterns that start with an ignored directory. These need to be
// rematched separately.
if (defaultIgnore.indexOf(pattern.split('/')[0]) >= 0) {
overrideDefaultIgnorePatterns.push(pattern);
}
});

// Same defaults as used for Chokidar.
if (!hasPositivePattern) {
mixedPatterns = ['package.json', '**/*.js'].concat(mixedPatterns);
}

return function (path) {
path = matchable(path);

// Ignore paths outside the current working directory. They can't be matched
// to a pattern.
if (/^\.\.\//.test(path)) {
return false;
}

var isSource = multimatch(path, mixedPatterns).length === 1;
if (!isSource) {
return false;
}

var isIgnored = multimatch(path, defaultIgnorePatterns).length === 1;
if (!isIgnored) {
return true;
}

var isErroneouslyIgnored = multimatch(path, overrideDefaultIgnorePatterns).length === 1;
if (isErroneouslyIgnored) {
return true;
}

return false;
};
}

function makeTestMatcher(files, excludePatterns) {
var initialPatterns = files.concat(excludePatterns);

return function (path) {
// Like in api.js, tests must be .js files and not start with _
if (nodePath.extname(path) !== '.js' || nodePath.basename(path)[0] === '_') {
return false;
}

// Check if the entire path matches a pattern.
if (multimatch(matchable(path), initialPatterns).length === 1) {
return true;
}

// Check if the path contains any directory components.
var dirname = nodePath.dirname(path);
if (dirname === '.') {
return false;
}

// Compute all possible subpaths. Note that the dirname is assumed to be
// relative to the working directory, without a leading `./`.
var subpaths = dirname.split(nodePath.sep).reduce(function (subpaths, component) {
var parent = subpaths[subpaths.length - 1];
if (parent) {
// Always use / to makes multimatch consistent across platforms.
subpaths.push(parent + '/' + component);
} else {
subpaths.push(component);
}
return subpaths;
}, []);

// Check if any of the possible subpaths match a pattern. If so, generate a
// new pattern with **/*.js.
var recursivePatterns = subpaths.filter(function (subpath) {
return multimatch(subpath, initialPatterns).length === 1;
}).map(function (subpath) {
// Always use / to makes multimatch consistent across platforms.
return subpath + '/**/*.js';
});

// See if the entire path matches any of the subpaths patterns, taking the
// excludePatterns into account. This mimicks the behavior in api.js
return multimatch(matchable(path), recursivePatterns.concat(excludePatterns)).length === 1;
};
}

function TestDependency(file, sources) {
this.file = file;
this.sources = sources;
Expand Down
Loading

0 comments on commit fdd7970

Please sign in to comment.