index.js 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186
  1. const path = require('path');
  2. const glob = require('glob');
  3. const minimatch = require('minimatch');
  4. const readPkgUp = require('read-pkg-up');
  5. const requireMainFilename = require('require-main-filename');
  6. class TestExclude {
  7. constructor(opts) {
  8. Object.assign(
  9. this,
  10. {
  11. cwd: process.cwd(),
  12. include: false,
  13. relativePath: true,
  14. configKey: null, // the key to load config from in package.json.
  15. configPath: null, // optionally override requireMainFilename.
  16. configFound: false,
  17. excludeNodeModules: true,
  18. extension: false
  19. },
  20. opts
  21. );
  22. if (typeof this.include === 'string') {
  23. this.include = [this.include];
  24. }
  25. if (typeof this.exclude === 'string') {
  26. this.exclude = [this.exclude];
  27. }
  28. if (typeof this.extension === 'string') {
  29. this.extension = [this.extension];
  30. } else if (
  31. !Array.isArray(this.extension) ||
  32. this.extension.length === 0
  33. ) {
  34. this.extension = false;
  35. }
  36. if (!this.include && !this.exclude && this.configKey) {
  37. Object.assign(this, this.pkgConf(this.configKey, this.configPath));
  38. }
  39. if (!this.exclude || !Array.isArray(this.exclude)) {
  40. this.exclude = exportFunc.defaultExclude;
  41. }
  42. if (this.include && this.include.length > 0) {
  43. this.include = prepGlobPatterns([].concat(this.include));
  44. } else {
  45. this.include = false;
  46. }
  47. if (
  48. this.excludeNodeModules &&
  49. !this.exclude.includes('**/node_modules/**')
  50. ) {
  51. this.exclude = this.exclude.concat('**/node_modules/**');
  52. }
  53. this.exclude = prepGlobPatterns([].concat(this.exclude));
  54. this.handleNegation();
  55. }
  56. /* handle the special case of negative globs
  57. * (!**foo/bar); we create a new this.excludeNegated set
  58. * of rules, which is applied after excludes and we
  59. * move excluded include rules into this.excludes.
  60. */
  61. handleNegation() {
  62. const noNeg = e => e.charAt(0) !== '!';
  63. const onlyNeg = e => e.charAt(0) === '!';
  64. const stripNeg = e => e.slice(1);
  65. if (Array.isArray(this.include)) {
  66. const includeNegated = this.include.filter(onlyNeg).map(stripNeg);
  67. this.exclude.push(...prepGlobPatterns(includeNegated));
  68. this.include = this.include.filter(noNeg);
  69. }
  70. this.excludeNegated = this.exclude.filter(onlyNeg).map(stripNeg);
  71. this.exclude = this.exclude.filter(noNeg);
  72. this.excludeNegated = prepGlobPatterns(this.excludeNegated);
  73. }
  74. shouldInstrument(filename, relFile) {
  75. if (
  76. this.extension &&
  77. !this.extension.some(ext => filename.endsWith(ext))
  78. ) {
  79. return false;
  80. }
  81. let pathToCheck = filename;
  82. if (this.relativePath) {
  83. relFile = relFile || path.relative(this.cwd, filename);
  84. // Don't instrument files that are outside of the current working directory.
  85. if (/^\.\./.test(path.relative(this.cwd, filename))) {
  86. return false;
  87. }
  88. pathToCheck = relFile.replace(/^\.[\\/]/, ''); // remove leading './' or '.\'.
  89. }
  90. const dot = { dot: true };
  91. const matches = pattern => minimatch(pathToCheck, pattern, dot);
  92. return (
  93. (!this.include || this.include.some(matches)) &&
  94. (!this.exclude.some(matches) || this.excludeNegated.some(matches))
  95. );
  96. }
  97. pkgConf(key, path) {
  98. const cwd = path || requireMainFilename(require);
  99. const obj = readPkgUp.sync({ cwd });
  100. if (obj.pkg && obj.pkg[key] && typeof obj.pkg[key] === 'object') {
  101. this.configFound = true;
  102. return obj.pkg[key];
  103. }
  104. return {};
  105. }
  106. globSync(cwd = this.cwd) {
  107. const globPatterns = getExtensionPattern(this.extension || []);
  108. const globOptions = { cwd, nodir: true, dot: true };
  109. /* If we don't have any excludeNegated then we can optimize glob by telling
  110. * it to not iterate into unwanted directory trees (like node_modules). */
  111. if (this.excludeNegated.length === 0) {
  112. globOptions.ignore = this.exclude;
  113. }
  114. return glob
  115. .sync(globPatterns, globOptions)
  116. .filter(file => this.shouldInstrument(path.resolve(cwd, file)));
  117. }
  118. }
  119. function prepGlobPatterns(patterns) {
  120. return patterns.reduce((result, pattern) => {
  121. // Allow gitignore style of directory exclusion
  122. if (!/\/\*\*$/.test(pattern)) {
  123. result = result.concat(pattern.replace(/\/$/, '') + '/**');
  124. }
  125. // Any rules of the form **/foo.js, should also match foo.js.
  126. if (/^\*\*\//.test(pattern)) {
  127. result = result.concat(pattern.replace(/^\*\*\//, ''));
  128. }
  129. return result.concat(pattern);
  130. }, []);
  131. }
  132. function getExtensionPattern(extension) {
  133. switch (extension.length) {
  134. case 0:
  135. return '**';
  136. case 1:
  137. return `**/*${extension[0]}`;
  138. default:
  139. return `**/*{${extension.join()}}`;
  140. }
  141. }
  142. const exportFunc = opts => new TestExclude(opts);
  143. const devConfigs = ['ava', 'babel', 'jest', 'nyc', 'rollup', 'webpack'];
  144. exportFunc.defaultExclude = [
  145. 'coverage/**',
  146. 'packages/*/test/**',
  147. 'test/**',
  148. 'test{,-*}.js',
  149. '**/*{.,-}test.js',
  150. '**/__tests__/**',
  151. `**/{${devConfigs.join()}}.config.js`
  152. ];
  153. module.exports = exportFunc;