You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
223 lines
6.7 KiB
223 lines
6.7 KiB
/** |
|
* @fileoverview `OverrideTester` class. |
|
* |
|
* `OverrideTester` class handles `files` property and `excludedFiles` property |
|
* of `overrides` config. |
|
* |
|
* It provides one method. |
|
* |
|
* - `test(filePath)` |
|
* Test if a file path matches the pair of `files` property and |
|
* `excludedFiles` property. The `filePath` argument must be an absolute |
|
* path. |
|
* |
|
* `ConfigArrayFactory` creates `OverrideTester` objects when it processes |
|
* `overrides` properties. |
|
* |
|
* @author Toru Nagashima <https://github.com/mysticatea> |
|
*/ |
|
"use strict"; |
|
|
|
const assert = require("assert"); |
|
const path = require("path"); |
|
const util = require("util"); |
|
const { Minimatch } = require("minimatch"); |
|
const minimatchOpts = { dot: true, matchBase: true }; |
|
|
|
/** |
|
* @typedef {Object} Pattern |
|
* @property {InstanceType<Minimatch>[] | null} includes The positive matchers. |
|
* @property {InstanceType<Minimatch>[] | null} excludes The negative matchers. |
|
*/ |
|
|
|
/** |
|
* Normalize a given pattern to an array. |
|
* @param {string|string[]|undefined} patterns A glob pattern or an array of glob patterns. |
|
* @returns {string[]|null} Normalized patterns. |
|
* @private |
|
*/ |
|
function normalizePatterns(patterns) { |
|
if (Array.isArray(patterns)) { |
|
return patterns.filter(Boolean); |
|
} |
|
if (typeof patterns === "string" && patterns) { |
|
return [patterns]; |
|
} |
|
return []; |
|
} |
|
|
|
/** |
|
* Create the matchers of given patterns. |
|
* @param {string[]} patterns The patterns. |
|
* @returns {InstanceType<Minimatch>[] | null} The matchers. |
|
*/ |
|
function toMatcher(patterns) { |
|
if (patterns.length === 0) { |
|
return null; |
|
} |
|
return patterns.map(pattern => { |
|
if (/^\.[/\\]/u.test(pattern)) { |
|
return new Minimatch( |
|
pattern.slice(2), |
|
|
|
// `./*.js` should not match with `subdir/foo.js` |
|
{ ...minimatchOpts, matchBase: false } |
|
); |
|
} |
|
return new Minimatch(pattern, minimatchOpts); |
|
}); |
|
} |
|
|
|
/** |
|
* Convert a given matcher to string. |
|
* @param {Pattern} matchers The matchers. |
|
* @returns {string} The string expression of the matcher. |
|
*/ |
|
function patternToJson({ includes, excludes }) { |
|
return { |
|
includes: includes && includes.map(m => m.pattern), |
|
excludes: excludes && excludes.map(m => m.pattern) |
|
}; |
|
} |
|
|
|
/** |
|
* The class to test given paths are matched by the patterns. |
|
*/ |
|
class OverrideTester { |
|
|
|
/** |
|
* Create a tester with given criteria. |
|
* If there are no criteria, returns `null`. |
|
* @param {string|string[]} files The glob patterns for included files. |
|
* @param {string|string[]} excludedFiles The glob patterns for excluded files. |
|
* @param {string} basePath The path to the base directory to test paths. |
|
* @returns {OverrideTester|null} The created instance or `null`. |
|
*/ |
|
static create(files, excludedFiles, basePath) { |
|
const includePatterns = normalizePatterns(files); |
|
const excludePatterns = normalizePatterns(excludedFiles); |
|
let endsWithWildcard = false; |
|
|
|
if (includePatterns.length === 0) { |
|
return null; |
|
} |
|
|
|
// Rejects absolute paths or relative paths to parents. |
|
for (const pattern of includePatterns) { |
|
if (path.isAbsolute(pattern) || pattern.includes("..")) { |
|
throw new Error(`Invalid override pattern (expected relative path not containing '..'): ${pattern}`); |
|
} |
|
if (pattern.endsWith("*")) { |
|
endsWithWildcard = true; |
|
} |
|
} |
|
for (const pattern of excludePatterns) { |
|
if (path.isAbsolute(pattern) || pattern.includes("..")) { |
|
throw new Error(`Invalid override pattern (expected relative path not containing '..'): ${pattern}`); |
|
} |
|
} |
|
|
|
const includes = toMatcher(includePatterns); |
|
const excludes = toMatcher(excludePatterns); |
|
|
|
return new OverrideTester( |
|
[{ includes, excludes }], |
|
basePath, |
|
endsWithWildcard |
|
); |
|
} |
|
|
|
/** |
|
* Combine two testers by logical and. |
|
* If either of the testers was `null`, returns the other tester. |
|
* The `basePath` property of the two must be the same value. |
|
* @param {OverrideTester|null} a A tester. |
|
* @param {OverrideTester|null} b Another tester. |
|
* @returns {OverrideTester|null} Combined tester. |
|
*/ |
|
static and(a, b) { |
|
if (!b) { |
|
return a && new OverrideTester( |
|
a.patterns, |
|
a.basePath, |
|
a.endsWithWildcard |
|
); |
|
} |
|
if (!a) { |
|
return new OverrideTester( |
|
b.patterns, |
|
b.basePath, |
|
b.endsWithWildcard |
|
); |
|
} |
|
|
|
assert.strictEqual(a.basePath, b.basePath); |
|
return new OverrideTester( |
|
a.patterns.concat(b.patterns), |
|
a.basePath, |
|
a.endsWithWildcard || b.endsWithWildcard |
|
); |
|
} |
|
|
|
/** |
|
* Initialize this instance. |
|
* @param {Pattern[]} patterns The matchers. |
|
* @param {string} basePath The base path. |
|
* @param {boolean} endsWithWildcard If `true` then a pattern ends with `*`. |
|
*/ |
|
constructor(patterns, basePath, endsWithWildcard = false) { |
|
|
|
/** @type {Pattern[]} */ |
|
this.patterns = patterns; |
|
|
|
/** @type {string} */ |
|
this.basePath = basePath; |
|
|
|
/** @type {boolean} */ |
|
this.endsWithWildcard = endsWithWildcard; |
|
} |
|
|
|
/** |
|
* Test if a given path is matched or not. |
|
* @param {string} filePath The absolute path to the target file. |
|
* @returns {boolean} `true` if the path was matched. |
|
*/ |
|
test(filePath) { |
|
if (typeof filePath !== "string" || !path.isAbsolute(filePath)) { |
|
throw new Error(`'filePath' should be an absolute path, but got ${filePath}.`); |
|
} |
|
const relativePath = path.relative(this.basePath, filePath); |
|
|
|
return this.patterns.every(({ includes, excludes }) => ( |
|
(!includes || includes.some(m => m.match(relativePath))) && |
|
(!excludes || !excludes.some(m => m.match(relativePath))) |
|
)); |
|
} |
|
|
|
// eslint-disable-next-line jsdoc/require-description |
|
/** |
|
* @returns {Object} a JSON compatible object. |
|
*/ |
|
toJSON() { |
|
if (this.patterns.length === 1) { |
|
return { |
|
...patternToJson(this.patterns[0]), |
|
basePath: this.basePath |
|
}; |
|
} |
|
return { |
|
AND: this.patterns.map(patternToJson), |
|
basePath: this.basePath |
|
}; |
|
} |
|
|
|
// eslint-disable-next-line jsdoc/require-description |
|
/** |
|
* @returns {Object} an object to display by `console.log()`. |
|
*/ |
|
[util.inspect.custom]() { |
|
return this.toJSON(); |
|
} |
|
} |
|
|
|
module.exports = { OverrideTester };
|
|
|