UNPKG

7.53 kBJavaScriptView Raw
1'use strict';
2const path = require('path');
3const globby = require('globby');
4const ignoreByDefault = require('ignore-by-default');
5const micromatch = require('micromatch');
6const slash = require('slash');
7
8const defaultIgnorePatterns = [...ignoreByDefault.directories(), '**/node_modules'];
9
10const buildExtensionPattern = extensions => extensions.length === 1 ? extensions[0] : `{${extensions.join(',')}}`;
11
12const normalizePatterns = patterns => {
13 // Always use `/` in patterns, harmonizing matching across platforms
14 if (process.platform === 'win32') {
15 patterns = patterns.map(pattern => slash(pattern));
16 }
17
18 return patterns.map(pattern => {
19 if (pattern.startsWith('./')) {
20 return pattern.slice(2);
21 }
22
23 if (pattern.startsWith('!./')) {
24 return `!${pattern.slice(3)}`;
25 }
26
27 return pattern;
28 });
29};
30
31function normalizeGlobs(testPatterns, helperPatterns, sourcePatterns, extensions) {
32 if (typeof testPatterns !== 'undefined' && (!Array.isArray(testPatterns) || testPatterns.length === 0)) {
33 throw new Error('The \'files\' configuration must be an array containing glob patterns.');
34 }
35
36 if (typeof helperPatterns !== 'undefined' && (!Array.isArray(helperPatterns) || helperPatterns.length === 0)) {
37 throw new Error('The \'helpers\' configuration must be an array containing glob patterns.');
38 }
39
40 if (sourcePatterns && (!Array.isArray(sourcePatterns) || sourcePatterns.length === 0)) {
41 throw new Error('The \'sources\' configuration must be an array containing glob patterns.');
42 }
43
44 const extensionPattern = buildExtensionPattern(extensions);
45 const defaultTestPatterns = [
46 `**/__tests__/**/*.${extensionPattern}`,
47 `**/*.spec.${extensionPattern}`,
48 `**/*.test.${extensionPattern}`,
49 `**/test-*.${extensionPattern}`,
50 `**/test.${extensionPattern}`,
51 `**/test/**/*.${extensionPattern}`,
52 `**/tests/**/*.${extensionPattern}`
53 ];
54
55 if (testPatterns) {
56 testPatterns = normalizePatterns(testPatterns);
57
58 if (testPatterns.every(pattern => pattern.startsWith('!'))) {
59 // Use defaults if patterns only contains exclusions.
60 testPatterns = [...defaultTestPatterns, ...testPatterns];
61 }
62 } else {
63 testPatterns = defaultTestPatterns;
64 }
65
66 if (helperPatterns) {
67 helperPatterns = normalizePatterns(helperPatterns);
68 } else {
69 helperPatterns = [];
70 }
71
72 const defaultSourcePatterns = [
73 '**/*.snap',
74 'ava.config.js',
75 'package.json',
76 `**/*.${extensionPattern}`
77 ];
78 if (sourcePatterns) {
79 sourcePatterns = normalizePatterns(sourcePatterns);
80
81 if (sourcePatterns.every(pattern => pattern.startsWith('!'))) {
82 // Use defaults if patterns only contains exclusions.
83 sourcePatterns = [...defaultSourcePatterns, ...sourcePatterns];
84 }
85 } else {
86 sourcePatterns = defaultSourcePatterns;
87 }
88
89 return {extensions, testPatterns, helperPatterns, sourcePatterns};
90}
91
92exports.normalizeGlobs = normalizeGlobs;
93
94const hasExtension = (extensions, file) => extensions.includes(path.extname(file).slice(1));
95
96exports.hasExtension = hasExtension;
97
98const findFiles = async (cwd, patterns) => {
99 const files = await globby(patterns, {
100 absolute: true,
101 brace: true,
102 case: false,
103 cwd,
104 dot: false,
105 expandDirectories: false,
106 extglob: true,
107 followSymlinkedDirectories: true,
108 gitignore: false,
109 globstar: true,
110 ignore: defaultIgnorePatterns,
111 matchBase: false,
112 onlyFiles: true,
113 stats: false,
114 unique: true
115 });
116
117 // `globby` returns slashes even on Windows. Normalize here so the file
118 // paths are consistently platform-accurate as tests are run.
119 if (process.platform === 'win32') {
120 return files.map(file => path.normalize(file));
121 }
122
123 return files;
124};
125
126async function findHelpersAndTests({cwd, extensions, testPatterns, helperPatterns}) {
127 // Search for tests concurrently with finding helpers.
128 const findingTests = findFiles(cwd, testPatterns);
129
130 const uniqueHelpers = new Set();
131 if (helperPatterns.length > 0) {
132 for (const file of await findFiles(cwd, helperPatterns)) {
133 if (!hasExtension(extensions, file)) {
134 continue;
135 }
136
137 uniqueHelpers.add(file);
138 }
139 }
140
141 const tests = [];
142 for (const file of await findingTests) {
143 if (!hasExtension(extensions, file)) {
144 continue;
145 }
146
147 if (path.basename(file).startsWith('_')) {
148 uniqueHelpers.add(file);
149 } else if (!uniqueHelpers.has(file)) { // Helpers cannot be tests.
150 tests.push(file);
151 }
152 }
153
154 return {helpers: [...uniqueHelpers], tests};
155}
156
157exports.findHelpersAndTests = findHelpersAndTests;
158
159async function findTests({cwd, extensions, testPatterns, helperPatterns}) {
160 const rejectHelpers = helperPatterns.length > 0;
161
162 const tests = [];
163 for (const file of await findFiles(cwd, testPatterns)) {
164 if (!hasExtension(extensions, file) || path.basename(file).startsWith('_')) {
165 continue;
166 }
167
168 if (rejectHelpers && matches(normalizeFileForMatching(cwd, file), helperPatterns)) {
169 continue;
170 }
171
172 tests.push(file);
173 }
174
175 return {tests};
176}
177
178exports.findTests = findTests;
179
180function getChokidarPatterns({sourcePatterns, testPatterns}) {
181 const paths = [];
182 const ignored = defaultIgnorePatterns.map(pattern => `${pattern}/**/*`);
183
184 for (const pattern of [...sourcePatterns, ...testPatterns]) {
185 if (!pattern.startsWith('!')) {
186 paths.push(pattern);
187 }
188 }
189
190 return {paths, ignored};
191}
192
193exports.getChokidarPatterns = getChokidarPatterns;
194
195const matchingCache = new WeakMap();
196const processMatchingPatterns = input => {
197 let result = matchingCache.get(input);
198 if (!result) {
199 const ignore = [
200 ...defaultIgnorePatterns,
201 // Unlike globby(), micromatch needs a complete pattern when ignoring directories.
202 ...defaultIgnorePatterns.map(pattern => `${pattern}/**/*`)
203 ];
204 const patterns = input.filter(pattern => {
205 if (pattern.startsWith('!')) {
206 // Unlike globby(), micromatch needs a complete pattern when ignoring directories.
207 ignore.push(pattern.slice(1), `${pattern.slice(1)}/**/*`);
208 return false;
209 }
210
211 return true;
212 });
213
214 result = {patterns, ignore};
215 matchingCache.set(input, result);
216 }
217
218 return result;
219};
220
221const matches = (file, patterns) => {
222 let ignore;
223 ({patterns, ignore} = processMatchingPatterns(patterns));
224 return micromatch.some(file, patterns, {ignore});
225};
226
227const NOT_IGNORED = ['**/*'];
228
229const normalizeFileForMatching = (cwd, file) => {
230 if (process.platform === 'win32') {
231 cwd = slash(cwd);
232 file = slash(file);
233 }
234
235 if (!cwd) { // TODO: Ensure tests provide an actual value.
236 return file;
237 }
238
239 // TODO: If `file` is outside `cwd` we can't normalize it. Need to figure
240 // out if that's a real-world scenario, but we may have to ensure the file
241 // isn't even selected.
242 if (!file.startsWith(cwd)) {
243 return file;
244 }
245
246 // Assume `cwd` does *not* end in a slash.
247 return file.slice(cwd.length + 1);
248};
249
250function classify(file, {cwd, extensions, helperPatterns, testPatterns, sourcePatterns}) {
251 let isHelper = false;
252 let isTest = false;
253 let isSource = false;
254
255 file = normalizeFileForMatching(cwd, file);
256
257 if (hasExtension(extensions, file)) {
258 if (path.basename(file).startsWith('_')) {
259 isHelper = matches(file, NOT_IGNORED);
260 } else {
261 isHelper = helperPatterns.length > 0 && matches(file, helperPatterns);
262
263 if (!isHelper) {
264 isTest = testPatterns.length > 0 && matches(file, testPatterns);
265
266 if (!isTest) {
267 // Note: Don't check sourcePatterns.length since we still need to
268 // check the file against the default ignore patterns.
269 isSource = matches(file, sourcePatterns);
270 }
271 }
272 }
273 } else {
274 isSource = matches(file, sourcePatterns);
275 }
276
277 return {isHelper, isTest, isSource};
278}
279
280exports.classify = classify;