1 | /**
|
---|
2 | * @fileoverview `OverrideTester` class.
|
---|
3 | *
|
---|
4 | * `OverrideTester` class handles `files` property and `excludedFiles` property
|
---|
5 | * of `overrides` config.
|
---|
6 | *
|
---|
7 | * It provides one method.
|
---|
8 | *
|
---|
9 | * - `test(filePath)`
|
---|
10 | * Test if a file path matches the pair of `files` property and
|
---|
11 | * `excludedFiles` property. The `filePath` argument must be an absolute
|
---|
12 | * path.
|
---|
13 | *
|
---|
14 | * `ConfigArrayFactory` creates `OverrideTester` objects when it processes
|
---|
15 | * `overrides` properties.
|
---|
16 | *
|
---|
17 | * @author Toru Nagashima <https://github.com/mysticatea>
|
---|
18 | */
|
---|
19 |
|
---|
20 | import assert from "assert";
|
---|
21 | import path from "path";
|
---|
22 | import util from "util";
|
---|
23 | import minimatch from "minimatch";
|
---|
24 |
|
---|
25 | const { Minimatch } = minimatch;
|
---|
26 |
|
---|
27 | const minimatchOpts = { dot: true, matchBase: true };
|
---|
28 |
|
---|
29 | /**
|
---|
30 | * @typedef {Object} Pattern
|
---|
31 | * @property {InstanceType<Minimatch>[] | null} includes The positive matchers.
|
---|
32 | * @property {InstanceType<Minimatch>[] | null} excludes The negative matchers.
|
---|
33 | */
|
---|
34 |
|
---|
35 | /**
|
---|
36 | * Normalize a given pattern to an array.
|
---|
37 | * @param {string|string[]|undefined} patterns A glob pattern or an array of glob patterns.
|
---|
38 | * @returns {string[]|null} Normalized patterns.
|
---|
39 | * @private
|
---|
40 | */
|
---|
41 | function normalizePatterns(patterns) {
|
---|
42 | if (Array.isArray(patterns)) {
|
---|
43 | return patterns.filter(Boolean);
|
---|
44 | }
|
---|
45 | if (typeof patterns === "string" && patterns) {
|
---|
46 | return [patterns];
|
---|
47 | }
|
---|
48 | return [];
|
---|
49 | }
|
---|
50 |
|
---|
51 | /**
|
---|
52 | * Create the matchers of given patterns.
|
---|
53 | * @param {string[]} patterns The patterns.
|
---|
54 | * @returns {InstanceType<Minimatch>[] | null} The matchers.
|
---|
55 | */
|
---|
56 | function toMatcher(patterns) {
|
---|
57 | if (patterns.length === 0) {
|
---|
58 | return null;
|
---|
59 | }
|
---|
60 | return patterns.map(pattern => {
|
---|
61 | if (/^\.[/\\]/u.test(pattern)) {
|
---|
62 | return new Minimatch(
|
---|
63 | pattern.slice(2),
|
---|
64 |
|
---|
65 | // `./*.js` should not match with `subdir/foo.js`
|
---|
66 | { ...minimatchOpts, matchBase: false }
|
---|
67 | );
|
---|
68 | }
|
---|
69 | return new Minimatch(pattern, minimatchOpts);
|
---|
70 | });
|
---|
71 | }
|
---|
72 |
|
---|
73 | /**
|
---|
74 | * Convert a given matcher to string.
|
---|
75 | * @param {Pattern} matchers The matchers.
|
---|
76 | * @returns {string} The string expression of the matcher.
|
---|
77 | */
|
---|
78 | function patternToJson({ includes, excludes }) {
|
---|
79 | return {
|
---|
80 | includes: includes && includes.map(m => m.pattern),
|
---|
81 | excludes: excludes && excludes.map(m => m.pattern)
|
---|
82 | };
|
---|
83 | }
|
---|
84 |
|
---|
85 | /**
|
---|
86 | * The class to test given paths are matched by the patterns.
|
---|
87 | */
|
---|
88 | class OverrideTester {
|
---|
89 |
|
---|
90 | /**
|
---|
91 | * Create a tester with given criteria.
|
---|
92 | * If there are no criteria, returns `null`.
|
---|
93 | * @param {string|string[]} files The glob patterns for included files.
|
---|
94 | * @param {string|string[]} excludedFiles The glob patterns for excluded files.
|
---|
95 | * @param {string} basePath The path to the base directory to test paths.
|
---|
96 | * @returns {OverrideTester|null} The created instance or `null`.
|
---|
97 | */
|
---|
98 | static create(files, excludedFiles, basePath) {
|
---|
99 | const includePatterns = normalizePatterns(files);
|
---|
100 | const excludePatterns = normalizePatterns(excludedFiles);
|
---|
101 | let endsWithWildcard = false;
|
---|
102 |
|
---|
103 | if (includePatterns.length === 0) {
|
---|
104 | return null;
|
---|
105 | }
|
---|
106 |
|
---|
107 | // Rejects absolute paths or relative paths to parents.
|
---|
108 | for (const pattern of includePatterns) {
|
---|
109 | if (path.isAbsolute(pattern) || pattern.includes("..")) {
|
---|
110 | throw new Error(`Invalid override pattern (expected relative path not containing '..'): ${pattern}`);
|
---|
111 | }
|
---|
112 | if (pattern.endsWith("*")) {
|
---|
113 | endsWithWildcard = true;
|
---|
114 | }
|
---|
115 | }
|
---|
116 | for (const pattern of excludePatterns) {
|
---|
117 | if (path.isAbsolute(pattern) || pattern.includes("..")) {
|
---|
118 | throw new Error(`Invalid override pattern (expected relative path not containing '..'): ${pattern}`);
|
---|
119 | }
|
---|
120 | }
|
---|
121 |
|
---|
122 | const includes = toMatcher(includePatterns);
|
---|
123 | const excludes = toMatcher(excludePatterns);
|
---|
124 |
|
---|
125 | return new OverrideTester(
|
---|
126 | [{ includes, excludes }],
|
---|
127 | basePath,
|
---|
128 | endsWithWildcard
|
---|
129 | );
|
---|
130 | }
|
---|
131 |
|
---|
132 | /**
|
---|
133 | * Combine two testers by logical and.
|
---|
134 | * If either of the testers was `null`, returns the other tester.
|
---|
135 | * The `basePath` property of the two must be the same value.
|
---|
136 | * @param {OverrideTester|null} a A tester.
|
---|
137 | * @param {OverrideTester|null} b Another tester.
|
---|
138 | * @returns {OverrideTester|null} Combined tester.
|
---|
139 | */
|
---|
140 | static and(a, b) {
|
---|
141 | if (!b) {
|
---|
142 | return a && new OverrideTester(
|
---|
143 | a.patterns,
|
---|
144 | a.basePath,
|
---|
145 | a.endsWithWildcard
|
---|
146 | );
|
---|
147 | }
|
---|
148 | if (!a) {
|
---|
149 | return new OverrideTester(
|
---|
150 | b.patterns,
|
---|
151 | b.basePath,
|
---|
152 | b.endsWithWildcard
|
---|
153 | );
|
---|
154 | }
|
---|
155 |
|
---|
156 | assert.strictEqual(a.basePath, b.basePath);
|
---|
157 | return new OverrideTester(
|
---|
158 | a.patterns.concat(b.patterns),
|
---|
159 | a.basePath,
|
---|
160 | a.endsWithWildcard || b.endsWithWildcard
|
---|
161 | );
|
---|
162 | }
|
---|
163 |
|
---|
164 | /**
|
---|
165 | * Initialize this instance.
|
---|
166 | * @param {Pattern[]} patterns The matchers.
|
---|
167 | * @param {string} basePath The base path.
|
---|
168 | * @param {boolean} endsWithWildcard If `true` then a pattern ends with `*`.
|
---|
169 | */
|
---|
170 | constructor(patterns, basePath, endsWithWildcard = false) {
|
---|
171 |
|
---|
172 | /** @type {Pattern[]} */
|
---|
173 | this.patterns = patterns;
|
---|
174 |
|
---|
175 | /** @type {string} */
|
---|
176 | this.basePath = basePath;
|
---|
177 |
|
---|
178 | /** @type {boolean} */
|
---|
179 | this.endsWithWildcard = endsWithWildcard;
|
---|
180 | }
|
---|
181 |
|
---|
182 | /**
|
---|
183 | * Test if a given path is matched or not.
|
---|
184 | * @param {string} filePath The absolute path to the target file.
|
---|
185 | * @returns {boolean} `true` if the path was matched.
|
---|
186 | */
|
---|
187 | test(filePath) {
|
---|
188 | if (typeof filePath !== "string" || !path.isAbsolute(filePath)) {
|
---|
189 | throw new Error(`'filePath' should be an absolute path, but got ${filePath}.`);
|
---|
190 | }
|
---|
191 | const relativePath = path.relative(this.basePath, filePath);
|
---|
192 |
|
---|
193 | return this.patterns.every(({ includes, excludes }) => (
|
---|
194 | (!includes || includes.some(m => m.match(relativePath))) &&
|
---|
195 | (!excludes || !excludes.some(m => m.match(relativePath)))
|
---|
196 | ));
|
---|
197 | }
|
---|
198 |
|
---|
199 | // eslint-disable-next-line jsdoc/require-description
|
---|
200 | /**
|
---|
201 | * @returns {Object} a JSON compatible object.
|
---|
202 | */
|
---|
203 | toJSON() {
|
---|
204 | if (this.patterns.length === 1) {
|
---|
205 | return {
|
---|
206 | ...patternToJson(this.patterns[0]),
|
---|
207 | basePath: this.basePath
|
---|
208 | };
|
---|
209 | }
|
---|
210 | return {
|
---|
211 | AND: this.patterns.map(patternToJson),
|
---|
212 | basePath: this.basePath
|
---|
213 | };
|
---|
214 | }
|
---|
215 |
|
---|
216 | // eslint-disable-next-line jsdoc/require-description
|
---|
217 | /**
|
---|
218 | * @returns {Object} an object to display by `console.log()`.
|
---|
219 | */
|
---|
220 | [util.inspect.custom]() {
|
---|
221 | return this.toJSON();
|
---|
222 | }
|
---|
223 | }
|
---|
224 |
|
---|
225 | export { OverrideTester };
|
---|