override-tester.js 6.8 KB

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