const path = require('path'); const glob = require('glob'); const minimatch = require('minimatch'); const readPkgUp = require('read-pkg-up'); const requireMainFilename = require('require-main-filename'); class TestExclude { constructor(opts) { Object.assign( this, { cwd: process.cwd(), include: false, relativePath: true, configKey: null, // the key to load config from in package.json. configPath: null, // optionally override requireMainFilename. configFound: false, excludeNodeModules: true, extension: false }, opts ); if (typeof this.include === 'string') { this.include = [this.include]; } if (typeof this.exclude === 'string') { this.exclude = [this.exclude]; } if (typeof this.extension === 'string') { this.extension = [this.extension]; } else if ( !Array.isArray(this.extension) || this.extension.length === 0 ) { this.extension = false; } if (!this.include && !this.exclude && this.configKey) { Object.assign(this, this.pkgConf(this.configKey, this.configPath)); } if (!this.exclude || !Array.isArray(this.exclude)) { this.exclude = exportFunc.defaultExclude; } if (this.include && this.include.length > 0) { this.include = prepGlobPatterns([].concat(this.include)); } else { this.include = false; } if ( this.excludeNodeModules && !this.exclude.includes('**/node_modules/**') ) { this.exclude = this.exclude.concat('**/node_modules/**'); } this.exclude = prepGlobPatterns([].concat(this.exclude)); this.handleNegation(); } /* handle the special case of negative globs * (!**foo/bar); we create a new this.excludeNegated set * of rules, which is applied after excludes and we * move excluded include rules into this.excludes. */ handleNegation() { const noNeg = e => e.charAt(0) !== '!'; const onlyNeg = e => e.charAt(0) === '!'; const stripNeg = e => e.slice(1); if (Array.isArray(this.include)) { const includeNegated = this.include.filter(onlyNeg).map(stripNeg); this.exclude.push(...prepGlobPatterns(includeNegated)); this.include = this.include.filter(noNeg); } this.excludeNegated = this.exclude.filter(onlyNeg).map(stripNeg); this.exclude = this.exclude.filter(noNeg); this.excludeNegated = prepGlobPatterns(this.excludeNegated); } shouldInstrument(filename, relFile) { if ( this.extension && !this.extension.some(ext => filename.endsWith(ext)) ) { return false; } let pathToCheck = filename; if (this.relativePath) { relFile = relFile || path.relative(this.cwd, filename); // Don't instrument files that are outside of the current working directory. if (/^\.\./.test(path.relative(this.cwd, filename))) { return false; } pathToCheck = relFile.replace(/^\.[\\/]/, ''); // remove leading './' or '.\'. } const dot = { dot: true }; const matches = pattern => minimatch(pathToCheck, pattern, dot); return ( (!this.include || this.include.some(matches)) && (!this.exclude.some(matches) || this.excludeNegated.some(matches)) ); } pkgConf(key, path) { const cwd = path || requireMainFilename(require); const obj = readPkgUp.sync({ cwd }); if (obj.pkg && obj.pkg[key] && typeof obj.pkg[key] === 'object') { this.configFound = true; return obj.pkg[key]; } return {}; } globSync(cwd = this.cwd) { const globPatterns = getExtensionPattern(this.extension || []); const globOptions = { cwd, nodir: true, dot: true }; /* If we don't have any excludeNegated then we can optimize glob by telling * it to not iterate into unwanted directory trees (like node_modules). */ if (this.excludeNegated.length === 0) { globOptions.ignore = this.exclude; } return glob .sync(globPatterns, globOptions) .filter(file => this.shouldInstrument(path.resolve(cwd, file))); } } function prepGlobPatterns(patterns) { return patterns.reduce((result, pattern) => { // Allow gitignore style of directory exclusion if (!/\/\*\*$/.test(pattern)) { result = result.concat(pattern.replace(/\/$/, '') + '/**'); } // Any rules of the form **/foo.js, should also match foo.js. if (/^\*\*\//.test(pattern)) { result = result.concat(pattern.replace(/^\*\*\//, '')); } return result.concat(pattern); }, []); } function getExtensionPattern(extension) { switch (extension.length) { case 0: return '**'; case 1: return `**/*${extension[0]}`; default: return `**/*{${extension.join()}}`; } } const exportFunc = opts => new TestExclude(opts); const devConfigs = ['ava', 'babel', 'jest', 'nyc', 'rollup', 'webpack']; exportFunc.defaultExclude = [ 'coverage/**', 'packages/*/test/**', 'test/**', 'test{,-*}.js', '**/*{.,-}test.js', '**/__tests__/**', `**/{${devConfigs.join()}}.config.js` ]; module.exports = exportFunc;