| 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; |