ava
Version:
248 lines (197 loc) • 7.7 kB
JavaScript
const path = require('path');
const globby = require('globby');
const ignoreByDefault = require('ignore-by-default');
const picomatch = require('picomatch');
const slash = require('slash');
const providerManager = require('./provider-manager');
const defaultIgnorePatterns = [...ignoreByDefault.directories(), '**/node_modules'];
const defaultPicomatchIgnorePatterns = [
...defaultIgnorePatterns,
// Unlike globby(), picomatch needs a complete pattern when ignoring directories.
...defaultIgnorePatterns.map(pattern => `${pattern}/**/*`)
];
const defaultMatchNoIgnore = picomatch(defaultPicomatchIgnorePatterns);
const defaultIgnoredByWatcherPatterns = [
'**/*.snap.md', // No need to rerun tests when the Markdown files change.
'ava.config.js', // Config is not reloaded so avoid rerunning tests when it changes.
'ava.config.cjs' // Config is not reloaded so avoid rerunning tests when it changes.
];
const buildExtensionPattern = extensions => extensions.length === 1 ? extensions[0] : `{${extensions.join(',')}}`;
function normalizePattern(pattern) {
// Always use `/` in patterns, harmonizing matching across platforms
if (process.platform === 'win32') {
pattern = slash(pattern);
}
if (pattern.startsWith('./')) {
return pattern.slice(2);
}
if (pattern.startsWith('!./')) {
return `!${pattern.slice(3)}`;
}
return pattern;
}
exports.normalizePattern = normalizePattern;
function normalizePatterns(patterns) {
return patterns.map(pattern => normalizePattern(pattern));
}
exports.normalizePatterns = normalizePatterns;
function normalizeGlobs({extensions, files: filePatterns, ignoredByWatcher: ignoredByWatcherPatterns, providers}) {
if (filePatterns !== undefined && (!Array.isArray(filePatterns) || filePatterns.length === 0)) {
throw new Error('The ’files’ configuration must be an array containing glob patterns.');
}
if (ignoredByWatcherPatterns !== undefined && (!Array.isArray(ignoredByWatcherPatterns) || ignoredByWatcherPatterns.length === 0)) {
throw new Error('The ’ignoredByWatcher’ configuration must be an array containing glob patterns.');
}
const extensionPattern = buildExtensionPattern(extensions);
const defaultTestPatterns = [
`test.${extensionPattern}`,
`{src,source}/test.${extensionPattern}`,
`**/__tests__/**/*.${extensionPattern}`,
`**/*.spec.${extensionPattern}`,
`**/*.test.${extensionPattern}`,
`**/test-*.${extensionPattern}`,
`**/test/**/*.${extensionPattern}`,
`**/tests/**/*.${extensionPattern}`,
'!**/__tests__/**/__{helper,fixture}?(s)__/**/*',
'!**/test?(s)/**/{helper,fixture}?(s)/**/*'
];
if (filePatterns) {
filePatterns = normalizePatterns(filePatterns);
if (filePatterns.every(pattern => pattern.startsWith('!'))) {
// Use defaults if patterns only contains exclusions.
filePatterns = [...defaultTestPatterns, ...filePatterns];
}
} else {
filePatterns = defaultTestPatterns;
}
if (ignoredByWatcherPatterns) {
ignoredByWatcherPatterns = [...defaultIgnoredByWatcherPatterns, ...normalizePatterns(ignoredByWatcherPatterns)];
} else {
ignoredByWatcherPatterns = [...defaultIgnoredByWatcherPatterns];
}
for (const {level, main} of providers) {
if (level >= providerManager.levels.pathRewrites) {
({filePatterns, ignoredByWatcherPatterns} = main.updateGlobs({filePatterns, ignoredByWatcherPatterns}));
}
}
return {extensions, filePatterns, ignoredByWatcherPatterns};
}
exports.normalizeGlobs = normalizeGlobs;
const hasExtension = (extensions, file) => extensions.includes(path.extname(file).slice(1));
exports.hasExtension = hasExtension;
const globFiles = async (cwd, patterns) => {
const files = await globby(patterns, {
// Globs should work relative to the cwd value only (this should be the
// project directory that AVA is run in).
absolute: false,
braceExpansion: true,
caseSensitiveMatch: false,
cwd,
dot: false,
expandDirectories: false,
extglob: true,
followSymbolicLinks: true,
gitignore: false,
globstar: true,
ignore: defaultIgnorePatterns,
baseNameMatch: false,
onlyFiles: true,
stats: false,
unique: true
});
// Return absolute file paths. This has the side-effect of normalizing paths
// on Windows.
return files.map(file => path.join(cwd, file));
};
async function findFiles({cwd, extensions, filePatterns}) {
return (await globFiles(cwd, filePatterns)).filter(file => hasExtension(extensions, file));
}
exports.findFiles = findFiles;
async function findTests({cwd, extensions, filePatterns}) {
return (await findFiles({cwd, extensions, filePatterns})).filter(file => !path.basename(file).startsWith('_'));
}
exports.findTests = findTests;
function getChokidarIgnorePatterns({ignoredByWatcherPatterns}) {
return [
...defaultIgnorePatterns.map(pattern => `${pattern}/**/*`),
...ignoredByWatcherPatterns.filter(pattern => !pattern.startsWith('!'))
];
}
exports.getChokidarIgnorePatterns = getChokidarIgnorePatterns;
const matchingCache = new WeakMap();
const processMatchingPatterns = input => {
let result = matchingCache.get(input);
if (!result) {
const ignore = [...defaultPicomatchIgnorePatterns];
const patterns = input.filter(pattern => {
if (pattern.startsWith('!')) {
// Unlike globby(), picomatch needs a complete pattern when ignoring directories.
ignore.push(pattern.slice(1), `${pattern.slice(1)}/**/*`);
return false;
}
return true;
});
result = {
match: picomatch(patterns, {ignore}),
matchNoIgnore: picomatch(patterns)
};
matchingCache.set(input, result);
}
return result;
};
function matches(file, patterns) {
const {match} = processMatchingPatterns(patterns);
return match(file);
}
exports.matches = matches;
const matchesIgnorePatterns = (file, patterns) => {
const {matchNoIgnore} = processMatchingPatterns(patterns);
return matchNoIgnore(file) || defaultMatchNoIgnore(file);
};
function normalizeFileForMatching(cwd, file) {
if (process.platform === 'win32') {
cwd = slash(cwd);
file = slash(file);
}
if (!cwd) { // TODO: Ensure tests provide an actual value.
return file;
}
// TODO: If `file` is outside `cwd` we can't normalize it. Need to figure
// out if that's a real-world scenario, but we may have to ensure the file
// isn't even selected.
if (!file.startsWith(cwd)) {
return file;
}
// Assume `cwd` does *not* end in a slash.
return file.slice(cwd.length + 1);
}
exports.normalizeFileForMatching = normalizeFileForMatching;
function isHelperish(file) { // Assume file has been normalized already.
// File names starting with an underscore are deemed "helpers".
if (path.basename(file).startsWith('_')) {
return true;
}
// This function assumes the file has been normalized. If it couldn't be,
// don't check if it's got a parent directory that starts with an underscore.
// Deem it not a "helper".
if (path.isAbsolute(file)) {
return false;
}
// If the file has a parent directory that starts with only a single
// underscore, it's deemed a "helper".
return path.dirname(file).split('/').some(dir => /^_(?:$|[^_])/.test(dir));
}
exports.isHelperish = isHelperish;
function classify(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;
function applyTestFileFilter({cwd, filter, testFiles}) {
return testFiles.filter(file => matches(normalizeFileForMatching(cwd, file), filter));
}
exports.applyTestFileFilter = applyTestFileFilter;
;