226 lines
6.7 KiB
JavaScript
226 lines
6.7 KiB
JavaScript
|
/**
|
||
|
* @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>
|
||
|
*/
|
||
|
|
||
|
import assert from "assert";
|
||
|
import path from "path";
|
||
|
import util from "util";
|
||
|
import minimatch from "minimatch";
|
||
|
|
||
|
const { Minimatch } = 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();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
export { OverrideTester };
|