ava-files.js 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293
  1. 'use strict';
  2. const fs = require('fs');
  3. const path = require('path');
  4. const Promise = require('bluebird');
  5. const slash = require('slash');
  6. const globby = require('globby');
  7. const flatten = require('lodash.flatten');
  8. const autoBind = require('auto-bind');
  9. const defaultIgnore = require('ignore-by-default').directories();
  10. const multimatch = require('multimatch');
  11. function handlePaths(files, extensions, excludePatterns, globOptions) {
  12. // Convert Promise to Bluebird
  13. files = Promise.resolve(globby(files.concat(excludePatterns), globOptions));
  14. const searchedParents = new Set();
  15. const foundFiles = new Set();
  16. function alreadySearchingParent(dir) {
  17. if (searchedParents.has(dir)) {
  18. return true;
  19. }
  20. const parentDir = path.dirname(dir);
  21. if (parentDir === dir) {
  22. // We have reached the root path
  23. return false;
  24. }
  25. return alreadySearchingParent(parentDir);
  26. }
  27. return files
  28. .map(file => {
  29. file = path.resolve(globOptions.cwd, file);
  30. if (fs.statSync(file).isDirectory()) {
  31. if (alreadySearchingParent(file)) {
  32. return null;
  33. }
  34. searchedParents.add(file);
  35. let pattern = path.join(file, '**', `*.${extensions.length === 1 ?
  36. extensions[0] : `{${extensions.join(',')}}`}`);
  37. if (process.platform === 'win32') {
  38. // Always use `/` in patterns, harmonizing matching across platforms
  39. pattern = slash(pattern);
  40. }
  41. return handlePaths([pattern], extensions, excludePatterns, globOptions);
  42. }
  43. // `globby` returns slashes even on Windows. Normalize here so the file
  44. // paths are consistently platform-accurate as tests are run.
  45. return path.normalize(file);
  46. })
  47. .then(flatten)
  48. .filter(file => file && extensions.includes(path.extname(file).substr(1)))
  49. .filter(file => {
  50. if (path.basename(file)[0] === '_' && globOptions.includeUnderscoredFiles !== true) {
  51. return false;
  52. }
  53. return true;
  54. })
  55. .map(file => path.resolve(file))
  56. .filter(file => {
  57. const alreadyFound = foundFiles.has(file);
  58. foundFiles.add(file);
  59. return !alreadyFound;
  60. });
  61. }
  62. const defaultExcludePatterns = () => [
  63. '!**/node_modules/**',
  64. '!**/fixtures/**',
  65. '!**/helpers/**'
  66. ];
  67. const defaultIncludePatterns = extPattern => [
  68. `test.${extPattern}`,
  69. `test-*.${extPattern}`,
  70. 'test', // Directory
  71. '**/__tests__', // Directory
  72. `**/*.test.${extPattern}`
  73. ];
  74. const defaultHelperPatterns = extPattern => [
  75. `**/__tests__/helpers/**/*.${extPattern}`,
  76. `**/__tests__/**/_*.${extPattern}`,
  77. `**/test/helpers/**/*.${extPattern}`,
  78. `**/test/**/_*.${extPattern}`
  79. ];
  80. const getDefaultIgnorePatterns = () => defaultIgnore.map(dir => `${dir}/**/*`);
  81. // Used on paths before they're passed to multimatch to harmonize matching
  82. // across platforms
  83. const matchable = process.platform === 'win32' ? slash : (path => path);
  84. class AvaFiles {
  85. constructor(options) {
  86. options = options || {};
  87. let files = (options.files || []).map(file => {
  88. // `./` should be removed from the beginning of patterns because
  89. // otherwise they won't match change events from Chokidar
  90. if (file.slice(0, 2) === './') {
  91. return file.slice(2);
  92. }
  93. return file;
  94. });
  95. this.extensions = options.extensions || ['js'];
  96. this.extensionPattern = this.extensions.length === 1 ?
  97. this.extensions[0] : `{${this.extensions.join(',')}}`;
  98. this.excludePatterns = defaultExcludePatterns();
  99. if (files.length === 0) {
  100. files = defaultIncludePatterns(this.extensionPattern);
  101. }
  102. this.files = files;
  103. this.sources = options.sources || [];
  104. this.cwd = options.cwd || process.cwd();
  105. this.globCaches = {
  106. cache: Object.create(null),
  107. statCache: Object.create(null),
  108. realpathCache: Object.create(null),
  109. symlinks: Object.create(null)
  110. };
  111. autoBind(this);
  112. }
  113. findTestFiles() {
  114. return handlePaths(this.files, this.extensions, this.excludePatterns, Object.assign({
  115. cwd: this.cwd,
  116. expandDirectories: false,
  117. nodir: false
  118. }, this.globCaches));
  119. }
  120. findTestHelpers() {
  121. return handlePaths(defaultHelperPatterns(this.extensionPattern), this.extensions, ['!**/node_modules/**'], Object.assign({
  122. cwd: this.cwd,
  123. includeUnderscoredFiles: true,
  124. expandDirectories: false,
  125. nodir: false
  126. }, this.globCaches));
  127. }
  128. isSource(filePath) {
  129. let mixedPatterns = [];
  130. const defaultIgnorePatterns = getDefaultIgnorePatterns(this.extensionPattern);
  131. const overrideDefaultIgnorePatterns = [];
  132. let hasPositivePattern = false;
  133. this.sources.forEach(pattern => {
  134. mixedPatterns.push(pattern);
  135. // TODO: Why not just `pattern[0] !== '!'`?
  136. if (!hasPositivePattern && pattern[0] !== '!') {
  137. hasPositivePattern = true;
  138. }
  139. // Extract patterns that start with an ignored directory. These need to be
  140. // rematched separately.
  141. if (defaultIgnore.indexOf(pattern.split('/')[0]) >= 0) {
  142. overrideDefaultIgnorePatterns.push(pattern);
  143. }
  144. });
  145. // Same defaults as used for Chokidar
  146. if (!hasPositivePattern) {
  147. mixedPatterns = ['package.json', '**/*.js'].concat(mixedPatterns);
  148. }
  149. filePath = matchable(filePath);
  150. // Ignore paths outside the current working directory.
  151. // They can't be matched to a pattern.
  152. if (/^\.\.\//.test(filePath)) {
  153. return false;
  154. }
  155. const isSource = multimatch(filePath, mixedPatterns).length === 1;
  156. if (!isSource) {
  157. return false;
  158. }
  159. const isIgnored = multimatch(filePath, defaultIgnorePatterns).length === 1;
  160. if (!isIgnored) {
  161. return true;
  162. }
  163. const isErroneouslyIgnored = multimatch(filePath, overrideDefaultIgnorePatterns).length === 1;
  164. if (isErroneouslyIgnored) {
  165. return true;
  166. }
  167. return false;
  168. }
  169. isTest(filePath) {
  170. const excludePatterns = this.excludePatterns;
  171. const initialPatterns = this.files.concat(excludePatterns);
  172. // Like in `api.js`, tests must be `.js` files and not start with `_`
  173. if (path.extname(filePath) !== '.js' || path.basename(filePath)[0] === '_') {
  174. return false;
  175. }
  176. // Check if the entire path matches a pattern
  177. if (multimatch(matchable(filePath), initialPatterns).length === 1) {
  178. return true;
  179. }
  180. // Check if the path contains any directory components
  181. const dirname = path.dirname(filePath);
  182. if (dirname === '.') {
  183. return false;
  184. }
  185. // Compute all possible subpaths. Note that the dirname is assumed to be
  186. // relative to the working directory, without a leading `./`.
  187. const subpaths = dirname.split(/[\\/]/).reduce((subpaths, component) => {
  188. const parent = subpaths[subpaths.length - 1];
  189. if (parent) {
  190. // Always use `/`` to makes multimatch consistent across platforms
  191. subpaths.push(`${parent}/${component}`);
  192. } else {
  193. subpaths.push(component);
  194. }
  195. return subpaths;
  196. }, []);
  197. // Check if any of the possible subpaths match a pattern. If so, generate a
  198. // new pattern with **/*.js.
  199. const recursivePatterns = subpaths
  200. .filter(subpath => multimatch(subpath, initialPatterns).length === 1)
  201. // Always use `/` to makes multimatch consistent across platforms
  202. .map(subpath => `${subpath}/**/*.js`);
  203. // See if the entire path matches any of the subpaths patterns, taking the
  204. // excludePatterns into account. This mimicks the behavior in api.js
  205. return multimatch(matchable(filePath), recursivePatterns.concat(excludePatterns)).length === 1;
  206. }
  207. getChokidarPatterns() {
  208. let paths = [];
  209. let ignored = [];
  210. this.sources.forEach(pattern => {
  211. if (pattern[0] === '!') {
  212. ignored.push(pattern.slice(1));
  213. } else {
  214. paths.push(pattern);
  215. }
  216. });
  217. // Allow source patterns to override the default ignore patterns. Chokidar
  218. // ignores paths that match the list of ignored patterns. It uses anymatch
  219. // under the hood, which supports negation patterns. For any source pattern
  220. // that starts with an ignored directory, ensure the corresponding negation
  221. // pattern is added to the ignored paths.
  222. const overrideDefaultIgnorePatterns = paths
  223. .filter(pattern => defaultIgnore.indexOf(pattern.split('/')[0]) >= 0)
  224. .map(pattern => `!${pattern}`);
  225. ignored = getDefaultIgnorePatterns().concat(ignored, overrideDefaultIgnorePatterns);
  226. if (paths.length === 0) {
  227. paths = ['package.json', '**/*.js', '**/*.snap'];
  228. }
  229. paths = paths.concat(this.files);
  230. return {
  231. paths,
  232. ignored
  233. };
  234. }
  235. }
  236. module.exports = AvaFiles;
  237. module.exports.defaultIncludePatterns = defaultIncludePatterns;
  238. module.exports.defaultExcludePatterns = defaultExcludePatterns;