UNPKG

7.7 kBJavaScriptView Raw
1'use strict';
2const path = require('path');
3const globby = require('globby');
4const ignoreByDefault = require('ignore-by-default');
5const picomatch = require('picomatch');
6const slash = require('slash');
7const providerManager = require('./provider-manager');
8
9const defaultIgnorePatterns = [...ignoreByDefault.directories(), '**/node_modules'];
10const defaultPicomatchIgnorePatterns = [
11 ...defaultIgnorePatterns,
12 // Unlike globby(), picomatch needs a complete pattern when ignoring directories.
13 ...defaultIgnorePatterns.map(pattern => `${pattern}/**/*`)
14];
15
16const defaultMatchNoIgnore = picomatch(defaultPicomatchIgnorePatterns);
17
18const defaultIgnoredByWatcherPatterns = [
19 '**/*.snap.md', // No need to rerun tests when the Markdown files change.
20 'ava.config.js', // Config is not reloaded so avoid rerunning tests when it changes.
21 'ava.config.cjs' // Config is not reloaded so avoid rerunning tests when it changes.
22];
23
24const buildExtensionPattern = extensions => extensions.length === 1 ? extensions[0] : `{${extensions.join(',')}}`;
25
26function normalizePattern(pattern) {
27 // Always use `/` in patterns, harmonizing matching across platforms
28 if (process.platform === 'win32') {
29 pattern = slash(pattern);
30 }
31
32 if (pattern.startsWith('./')) {
33 return pattern.slice(2);
34 }
35
36 if (pattern.startsWith('!./')) {
37 return `!${pattern.slice(3)}`;
38 }
39
40 return pattern;
41}
42
43exports.normalizePattern = normalizePattern;
44
45function normalizePatterns(patterns) {
46 return patterns.map(pattern => normalizePattern(pattern));
47}
48
49exports.normalizePatterns = normalizePatterns;
50
51function normalizeGlobs({extensions, files: filePatterns, ignoredByWatcher: ignoredByWatcherPatterns, providers}) {
52 if (filePatterns !== undefined && (!Array.isArray(filePatterns) || filePatterns.length === 0)) {
53 throw new Error('The ’files’ configuration must be an array containing glob patterns.');
54 }
55
56 if (ignoredByWatcherPatterns !== undefined && (!Array.isArray(ignoredByWatcherPatterns) || ignoredByWatcherPatterns.length === 0)) {
57 throw new Error('The ’ignoredByWatcher’ configuration must be an array containing glob patterns.');
58 }
59
60 const extensionPattern = buildExtensionPattern(extensions);
61 const defaultTestPatterns = [
62 `test.${extensionPattern}`,
63 `{src,source}/test.${extensionPattern}`,
64 `**/__tests__/**/*.${extensionPattern}`,
65 `**/*.spec.${extensionPattern}`,
66 `**/*.test.${extensionPattern}`,
67 `**/test-*.${extensionPattern}`,
68 `**/test/**/*.${extensionPattern}`,
69 `**/tests/**/*.${extensionPattern}`,
70 '!**/__tests__/**/__{helper,fixture}?(s)__/**/*',
71 '!**/test?(s)/**/{helper,fixture}?(s)/**/*'
72 ];
73
74 if (filePatterns) {
75 filePatterns = normalizePatterns(filePatterns);
76
77 if (filePatterns.every(pattern => pattern.startsWith('!'))) {
78 // Use defaults if patterns only contains exclusions.
79 filePatterns = [...defaultTestPatterns, ...filePatterns];
80 }
81 } else {
82 filePatterns = defaultTestPatterns;
83 }
84
85 if (ignoredByWatcherPatterns) {
86 ignoredByWatcherPatterns = [...defaultIgnoredByWatcherPatterns, ...normalizePatterns(ignoredByWatcherPatterns)];
87 } else {
88 ignoredByWatcherPatterns = [...defaultIgnoredByWatcherPatterns];
89 }
90
91 for (const {level, main} of providers) {
92 if (level >= providerManager.levels.pathRewrites) {
93 ({filePatterns, ignoredByWatcherPatterns} = main.updateGlobs({filePatterns, ignoredByWatcherPatterns}));
94 }
95 }
96
97 return {extensions, filePatterns, ignoredByWatcherPatterns};
98}
99
100exports.normalizeGlobs = normalizeGlobs;
101
102const hasExtension = (extensions, file) => extensions.includes(path.extname(file).slice(1));
103
104exports.hasExtension = hasExtension;
105
106const globFiles = async (cwd, patterns) => {
107 const files = await globby(patterns, {
108 // Globs should work relative to the cwd value only (this should be the
109 // project directory that AVA is run in).
110 absolute: false,
111 braceExpansion: true,
112 caseSensitiveMatch: false,
113 cwd,
114 dot: false,
115 expandDirectories: false,
116 extglob: true,
117 followSymbolicLinks: true,
118 gitignore: false,
119 globstar: true,
120 ignore: defaultIgnorePatterns,
121 baseNameMatch: false,
122 onlyFiles: true,
123 stats: false,
124 unique: true
125 });
126
127 // Return absolute file paths. This has the side-effect of normalizing paths
128 // on Windows.
129 return files.map(file => path.join(cwd, file));
130};
131
132async function findFiles({cwd, extensions, filePatterns}) {
133 return (await globFiles(cwd, filePatterns)).filter(file => hasExtension(extensions, file));
134}
135
136exports.findFiles = findFiles;
137
138async function findTests({cwd, extensions, filePatterns}) {
139 return (await findFiles({cwd, extensions, filePatterns})).filter(file => !path.basename(file).startsWith('_'));
140}
141
142exports.findTests = findTests;
143
144function getChokidarIgnorePatterns({ignoredByWatcherPatterns}) {
145 return [
146 ...defaultIgnorePatterns.map(pattern => `${pattern}/**/*`),
147 ...ignoredByWatcherPatterns.filter(pattern => !pattern.startsWith('!'))
148 ];
149}
150
151exports.getChokidarIgnorePatterns = getChokidarIgnorePatterns;
152
153const matchingCache = new WeakMap();
154const processMatchingPatterns = input => {
155 let result = matchingCache.get(input);
156 if (!result) {
157 const ignore = [...defaultPicomatchIgnorePatterns];
158 const patterns = input.filter(pattern => {
159 if (pattern.startsWith('!')) {
160 // Unlike globby(), picomatch needs a complete pattern when ignoring directories.
161 ignore.push(pattern.slice(1), `${pattern.slice(1)}/**/*`);
162 return false;
163 }
164
165 return true;
166 });
167
168 result = {
169 match: picomatch(patterns, {ignore}),
170 matchNoIgnore: picomatch(patterns)
171 };
172 matchingCache.set(input, result);
173 }
174
175 return result;
176};
177
178function matches(file, patterns) {
179 const {match} = processMatchingPatterns(patterns);
180 return match(file);
181}
182
183exports.matches = matches;
184
185const matchesIgnorePatterns = (file, patterns) => {
186 const {matchNoIgnore} = processMatchingPatterns(patterns);
187 return matchNoIgnore(file) || defaultMatchNoIgnore(file);
188};
189
190function normalizeFileForMatching(cwd, file) {
191 if (process.platform === 'win32') {
192 cwd = slash(cwd);
193 file = slash(file);
194 }
195
196 if (!cwd) { // TODO: Ensure tests provide an actual value.
197 return file;
198 }
199
200 // TODO: If `file` is outside `cwd` we can't normalize it. Need to figure
201 // out if that's a real-world scenario, but we may have to ensure the file
202 // isn't even selected.
203 if (!file.startsWith(cwd)) {
204 return file;
205 }
206
207 // Assume `cwd` does *not* end in a slash.
208 return file.slice(cwd.length + 1);
209}
210
211exports.normalizeFileForMatching = normalizeFileForMatching;
212
213function isHelperish(file) { // Assume file has been normalized already.
214 // File names starting with an underscore are deemed "helpers".
215 if (path.basename(file).startsWith('_')) {
216 return true;
217 }
218
219 // This function assumes the file has been normalized. If it couldn't be,
220 // don't check if it's got a parent directory that starts with an underscore.
221 // Deem it not a "helper".
222 if (path.isAbsolute(file)) {
223 return false;
224 }
225
226 // If the file has a parent directory that starts with only a single
227 // underscore, it's deemed a "helper".
228 return path.dirname(file).split('/').some(dir => /^_(?:$|[^_])/.test(dir));
229}
230
231exports.isHelperish = isHelperish;
232
233function classify(file, {cwd, extensions, filePatterns, ignoredByWatcherPatterns}) {
234 file = normalizeFileForMatching(cwd, file);
235 return {
236 isIgnoredByWatcher: matchesIgnorePatterns(file, ignoredByWatcherPatterns),
237 isTest: hasExtension(extensions, file) && !isHelperish(file) && filePatterns.length > 0 && matches(file, filePatterns)
238 };
239}
240
241exports.classify = classify;
242
243function applyTestFileFilter({cwd, filter, testFiles}) {
244 return testFiles.filter(file => matches(normalizeFileForMatching(cwd, file), filter));
245}
246
247exports.applyTestFileFilter = applyTestFileFilter;