api.js 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979
  1. 'use strict';
  2. Object.defineProperty(exports, '__esModule', { value: true });
  3. function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; }
  4. var path = _interopDefault(require('path'));
  5. var minimatch = _interopDefault(require('minimatch'));
  6. var createDebug = _interopDefault(require('debug'));
  7. var objectSchema = require('@humanwhocodes/object-schema');
  8. /**
  9. * @fileoverview ConfigSchema
  10. * @author Nicholas C. Zakas
  11. */
  12. //------------------------------------------------------------------------------
  13. // Helpers
  14. //------------------------------------------------------------------------------
  15. /**
  16. * Assets that a given value is an array.
  17. * @param {*} value The value to check.
  18. * @returns {void}
  19. * @throws {TypeError} When the value is not an array.
  20. */
  21. function assertIsArray(value) {
  22. if (!Array.isArray(value)) {
  23. throw new TypeError('Expected value to be an array.');
  24. }
  25. }
  26. /**
  27. * Assets that a given value is an array containing only strings and functions.
  28. * @param {*} value The value to check.
  29. * @returns {void}
  30. * @throws {TypeError} When the value is not an array of strings and functions.
  31. */
  32. function assertIsArrayOfStringsAndFunctions(value, name) {
  33. assertIsArray(value);
  34. if (value.some(item => typeof item !== 'string' && typeof item !== 'function')) {
  35. throw new TypeError('Expected array to only contain strings.');
  36. }
  37. }
  38. //------------------------------------------------------------------------------
  39. // Exports
  40. //------------------------------------------------------------------------------
  41. /**
  42. * The base schema that every ConfigArray uses.
  43. * @type Object
  44. */
  45. const baseSchema = Object.freeze({
  46. name: {
  47. required: false,
  48. merge() {
  49. return undefined;
  50. },
  51. validate(value) {
  52. if (typeof value !== 'string') {
  53. throw new TypeError('Property must be a string.');
  54. }
  55. }
  56. },
  57. files: {
  58. required: false,
  59. merge() {
  60. return undefined;
  61. },
  62. validate(value) {
  63. // first check if it's an array
  64. assertIsArray(value);
  65. // then check each member
  66. value.forEach(item => {
  67. if (Array.isArray(item)) {
  68. assertIsArrayOfStringsAndFunctions(item);
  69. } else if (typeof item !== 'string' && typeof item !== 'function') {
  70. throw new TypeError('Items must be a string, a function, or an array of strings and functions.');
  71. }
  72. });
  73. }
  74. },
  75. ignores: {
  76. required: false,
  77. merge() {
  78. return undefined;
  79. },
  80. validate: assertIsArrayOfStringsAndFunctions
  81. }
  82. });
  83. /**
  84. * @fileoverview ConfigArray
  85. * @author Nicholas C. Zakas
  86. */
  87. //------------------------------------------------------------------------------
  88. // Helpers
  89. //------------------------------------------------------------------------------
  90. const Minimatch = minimatch.Minimatch;
  91. const minimatchCache = new Map();
  92. const negatedMinimatchCache = new Map();
  93. const debug = createDebug('@hwc/config-array');
  94. const MINIMATCH_OPTIONS = {
  95. // matchBase: true,
  96. dot: true
  97. };
  98. const CONFIG_TYPES = new Set(['array', 'function']);
  99. /**
  100. * Shorthand for checking if a value is a string.
  101. * @param {any} value The value to check.
  102. * @returns {boolean} True if a string, false if not.
  103. */
  104. function isString(value) {
  105. return typeof value === 'string';
  106. }
  107. /**
  108. * Asserts that the files key of a config object is a nonempty array.
  109. * @param {object} config The config object to check.
  110. * @returns {void}
  111. * @throws {TypeError} If the files key isn't a nonempty array.
  112. */
  113. function assertNonEmptyFilesArray(config) {
  114. if (!Array.isArray(config.files) || config.files.length === 0) {
  115. throw new TypeError('The files key must be a non-empty array.');
  116. }
  117. }
  118. /**
  119. * Wrapper around minimatch that caches minimatch patterns for
  120. * faster matching speed over multiple file path evaluations.
  121. * @param {string} filepath The file path to match.
  122. * @param {string} pattern The glob pattern to match against.
  123. * @param {object} options The minimatch options to use.
  124. * @returns
  125. */
  126. function doMatch(filepath, pattern, options = {}) {
  127. let cache = minimatchCache;
  128. if (options.flipNegate) {
  129. cache = negatedMinimatchCache;
  130. }
  131. let matcher = cache.get(pattern);
  132. if (!matcher) {
  133. matcher = new Minimatch(pattern, Object.assign({}, MINIMATCH_OPTIONS, options));
  134. cache.set(pattern, matcher);
  135. }
  136. return matcher.match(filepath);
  137. }
  138. /**
  139. * Normalizes a `ConfigArray` by flattening it and executing any functions
  140. * that are found inside.
  141. * @param {Array} items The items in a `ConfigArray`.
  142. * @param {Object} context The context object to pass into any function
  143. * found.
  144. * @param {Array<string>} extraConfigTypes The config types to check.
  145. * @returns {Promise<Array>} A flattened array containing only config objects.
  146. * @throws {TypeError} When a config function returns a function.
  147. */
  148. async function normalize(items, context, extraConfigTypes) {
  149. const allowFunctions = extraConfigTypes.includes('function');
  150. const allowArrays = extraConfigTypes.includes('array');
  151. async function* flatTraverse(array) {
  152. for (let item of array) {
  153. if (typeof item === 'function') {
  154. if (!allowFunctions) {
  155. throw new TypeError('Unexpected function.');
  156. }
  157. item = item(context);
  158. if (item.then) {
  159. item = await item;
  160. }
  161. }
  162. if (Array.isArray(item)) {
  163. if (!allowArrays) {
  164. throw new TypeError('Unexpected array.');
  165. }
  166. yield* flatTraverse(item);
  167. } else if (typeof item === 'function') {
  168. throw new TypeError('A config function can only return an object or array.');
  169. } else {
  170. yield item;
  171. }
  172. }
  173. }
  174. /*
  175. * Async iterables cannot be used with the spread operator, so we need to manually
  176. * create the array to return.
  177. */
  178. const asyncIterable = await flatTraverse(items);
  179. const configs = [];
  180. for await (const config of asyncIterable) {
  181. configs.push(config);
  182. }
  183. return configs;
  184. }
  185. /**
  186. * Normalizes a `ConfigArray` by flattening it and executing any functions
  187. * that are found inside.
  188. * @param {Array} items The items in a `ConfigArray`.
  189. * @param {Object} context The context object to pass into any function
  190. * found.
  191. * @param {Array<string>} extraConfigTypes The config types to check.
  192. * @returns {Array} A flattened array containing only config objects.
  193. * @throws {TypeError} When a config function returns a function.
  194. */
  195. function normalizeSync(items, context, extraConfigTypes) {
  196. const allowFunctions = extraConfigTypes.includes('function');
  197. const allowArrays = extraConfigTypes.includes('array');
  198. function* flatTraverse(array) {
  199. for (let item of array) {
  200. if (typeof item === 'function') {
  201. if (!allowFunctions) {
  202. throw new TypeError('Unexpected function.');
  203. }
  204. item = item(context);
  205. if (item.then) {
  206. throw new TypeError('Async config functions are not supported.');
  207. }
  208. }
  209. if (Array.isArray(item)) {
  210. if (!allowArrays) {
  211. throw new TypeError('Unexpected array.');
  212. }
  213. yield* flatTraverse(item);
  214. } else if (typeof item === 'function') {
  215. throw new TypeError('A config function can only return an object or array.');
  216. } else {
  217. yield item;
  218. }
  219. }
  220. }
  221. return [...flatTraverse(items)];
  222. }
  223. /**
  224. * Determines if a given file path should be ignored based on the given
  225. * matcher.
  226. * @param {Array<string|() => boolean>} ignores The ignore patterns to check.
  227. * @param {string} filePath The absolute path of the file to check.
  228. * @param {string} relativeFilePath The relative path of the file to check.
  229. * @returns {boolean} True if the path should be ignored and false if not.
  230. */
  231. function shouldIgnorePath(ignores, filePath, relativeFilePath) {
  232. // all files outside of the basePath are ignored
  233. if (relativeFilePath.startsWith('..')) {
  234. return true;
  235. }
  236. return ignores.reduce((ignored, matcher) => {
  237. if (!ignored) {
  238. if (typeof matcher === 'function') {
  239. return matcher(filePath);
  240. }
  241. // don't check negated patterns because we're not ignored yet
  242. if (!matcher.startsWith('!')) {
  243. return doMatch(relativeFilePath, matcher);
  244. }
  245. // otherwise we're still not ignored
  246. return false;
  247. }
  248. // only need to check negated patterns because we're ignored
  249. if (typeof matcher === 'string' && matcher.startsWith('!')) {
  250. return !doMatch(relativeFilePath, matcher, {
  251. flipNegate: true
  252. });
  253. }
  254. return ignored;
  255. }, false);
  256. }
  257. /**
  258. * Determines if a given file path is matched by a config. If the config
  259. * has no `files` field, then it matches; otherwise, if a `files` field
  260. * is present then we match the globs in `files` and exclude any globs in
  261. * `ignores`.
  262. * @param {string} filePath The absolute file path to check.
  263. * @param {Object} config The config object to check.
  264. * @returns {boolean} True if the file path is matched by the config,
  265. * false if not.
  266. */
  267. function pathMatches(filePath, basePath, config) {
  268. /*
  269. * For both files and ignores, functions are passed the absolute
  270. * file path while strings are compared against the relative
  271. * file path.
  272. */
  273. const relativeFilePath = path.relative(basePath, filePath);
  274. // if files isn't an array, throw an error
  275. assertNonEmptyFilesArray(config);
  276. // match both strings and functions
  277. const match = pattern => {
  278. if (isString(pattern)) {
  279. return doMatch(relativeFilePath, pattern);
  280. }
  281. if (typeof pattern === 'function') {
  282. return pattern(filePath);
  283. }
  284. throw new TypeError(`Unexpected matcher type ${pattern}.`);
  285. };
  286. // check for all matches to config.files
  287. let filePathMatchesPattern = config.files.some(pattern => {
  288. if (Array.isArray(pattern)) {
  289. return pattern.every(match);
  290. }
  291. return match(pattern);
  292. });
  293. /*
  294. * If the file path matches the config.files patterns, then check to see
  295. * if there are any files to ignore.
  296. */
  297. if (filePathMatchesPattern && config.ignores) {
  298. filePathMatchesPattern = !shouldIgnorePath(config.ignores, filePath, relativeFilePath);
  299. }
  300. return filePathMatchesPattern;
  301. }
  302. /**
  303. * Ensures that a ConfigArray has been normalized.
  304. * @param {ConfigArray} configArray The ConfigArray to check.
  305. * @returns {void}
  306. * @throws {Error} When the `ConfigArray` is not normalized.
  307. */
  308. function assertNormalized(configArray) {
  309. // TODO: Throw more verbose error
  310. if (!configArray.isNormalized()) {
  311. throw new Error('ConfigArray must be normalized to perform this operation.');
  312. }
  313. }
  314. /**
  315. * Ensures that config types are valid.
  316. * @param {Array<string>} extraConfigTypes The config types to check.
  317. * @returns {void}
  318. * @throws {Error} When the config types array is invalid.
  319. */
  320. function assertExtraConfigTypes(extraConfigTypes) {
  321. if (extraConfigTypes.length > 2) {
  322. throw new TypeError('configTypes must be an array with at most two items.');
  323. }
  324. for (const configType of extraConfigTypes) {
  325. if (!CONFIG_TYPES.has(configType)) {
  326. throw new TypeError(`Unexpected config type "${configType}" found. Expected one of: "object", "array", "function".`);
  327. }
  328. }
  329. }
  330. //------------------------------------------------------------------------------
  331. // Public Interface
  332. //------------------------------------------------------------------------------
  333. const ConfigArraySymbol = {
  334. isNormalized: Symbol('isNormalized'),
  335. configCache: Symbol('configCache'),
  336. schema: Symbol('schema'),
  337. finalizeConfig: Symbol('finalizeConfig'),
  338. preprocessConfig: Symbol('preprocessConfig')
  339. };
  340. // used to store calculate data for faster lookup
  341. const dataCache = new WeakMap();
  342. /**
  343. * Represents an array of config objects and provides method for working with
  344. * those config objects.
  345. */
  346. class ConfigArray extends Array {
  347. /**
  348. * Creates a new instance of ConfigArray.
  349. * @param {Iterable|Function|Object} configs An iterable yielding config
  350. * objects, or a config function, or a config object.
  351. * @param {string} [options.basePath=""] The path of the config file
  352. * @param {boolean} [options.normalized=false] Flag indicating if the
  353. * configs have already been normalized.
  354. * @param {Object} [options.schema] The additional schema
  355. * definitions to use for the ConfigArray schema.
  356. * @param {Array<string>} [options.configTypes] List of config types supported.
  357. */
  358. constructor(configs, {
  359. basePath = '',
  360. normalized = false,
  361. schema: customSchema,
  362. extraConfigTypes = []
  363. } = {}
  364. ) {
  365. super();
  366. /**
  367. * Tracks if the array has been normalized.
  368. * @property isNormalized
  369. * @type boolean
  370. * @private
  371. */
  372. this[ConfigArraySymbol.isNormalized] = normalized;
  373. /**
  374. * The schema used for validating and merging configs.
  375. * @property schema
  376. * @type ObjectSchema
  377. * @private
  378. */
  379. this[ConfigArraySymbol.schema] = new objectSchema.ObjectSchema(
  380. Object.assign({}, customSchema, baseSchema)
  381. );
  382. /**
  383. * The path of the config file that this array was loaded from.
  384. * This is used to calculate filename matches.
  385. * @property basePath
  386. * @type string
  387. */
  388. this.basePath = basePath;
  389. assertExtraConfigTypes(extraConfigTypes);
  390. /**
  391. * The supported config types.
  392. * @property configTypes
  393. * @type Array<string>
  394. */
  395. this.extraConfigTypes = Object.freeze([...extraConfigTypes]);
  396. /**
  397. * A cache to store calculated configs for faster repeat lookup.
  398. * @property configCache
  399. * @type Map
  400. * @private
  401. */
  402. this[ConfigArraySymbol.configCache] = new Map();
  403. // init cache
  404. dataCache.set(this, {
  405. explicitMatches: new Map(),
  406. directoryMatches: new Map(),
  407. files: undefined,
  408. ignores: undefined
  409. });
  410. // load the configs into this array
  411. if (Array.isArray(configs)) {
  412. this.push(...configs);
  413. } else {
  414. this.push(configs);
  415. }
  416. }
  417. /**
  418. * Prevent normal array methods from creating a new `ConfigArray` instance.
  419. * This is to ensure that methods such as `slice()` won't try to create a
  420. * new instance of `ConfigArray` behind the scenes as doing so may throw
  421. * an error due to the different constructor signature.
  422. * @returns {Function} The `Array` constructor.
  423. */
  424. static get [Symbol.species]() {
  425. return Array;
  426. }
  427. /**
  428. * Returns the `files` globs from every config object in the array.
  429. * This can be used to determine which files will be matched by a
  430. * config array or to use as a glob pattern when no patterns are provided
  431. * for a command line interface.
  432. * @returns {Array<string|Function>} An array of matchers.
  433. */
  434. get files() {
  435. assertNormalized(this);
  436. // if this data has been cached, retrieve it
  437. const cache = dataCache.get(this);
  438. if (cache.files) {
  439. return cache.files;
  440. }
  441. // otherwise calculate it
  442. const result = [];
  443. for (const config of this) {
  444. if (config.files) {
  445. config.files.forEach(filePattern => {
  446. result.push(filePattern);
  447. });
  448. }
  449. }
  450. // store result
  451. cache.files = result;
  452. dataCache.set(this, cache);
  453. return result;
  454. }
  455. /**
  456. * Returns ignore matchers that should always be ignored regardless of
  457. * the matching `files` fields in any configs. This is necessary to mimic
  458. * the behavior of things like .gitignore and .eslintignore, allowing a
  459. * globbing operation to be faster.
  460. * @returns {string[]} An array of string patterns and functions to be ignored.
  461. */
  462. get ignores() {
  463. assertNormalized(this);
  464. // if this data has been cached, retrieve it
  465. const cache = dataCache.get(this);
  466. if (cache.ignores) {
  467. return cache.ignores;
  468. }
  469. // otherwise calculate it
  470. const result = [];
  471. for (const config of this) {
  472. /*
  473. * We only count ignores if there are no other keys in the object.
  474. * In this case, it acts list a globally ignored pattern. If there
  475. * are additional keys, then ignores act like exclusions.
  476. */
  477. if (config.ignores && Object.keys(config).length === 1) {
  478. /*
  479. * If there are directory ignores, then we need to double up
  480. * the patterns to be ignored. For instance, `foo` will also
  481. * need `foo/**` in order to account for subdirectories.
  482. */
  483. config.ignores.forEach(ignore => {
  484. result.push(ignore);
  485. if (typeof ignore === 'string') {
  486. // unignoring files won't work unless we unignore directories too
  487. if (ignore.startsWith('!')) {
  488. if (ignore.endsWith('/**')) {
  489. result.push(ignore.slice(0, ignore.length - 3));
  490. } else if (ignore.endsWith('/*')) {
  491. result.push(ignore.slice(0, ignore.length - 2));
  492. }
  493. }
  494. // directories should work with or without a trailing slash
  495. if (ignore.endsWith('/')) {
  496. result.push(ignore.slice(0, ignore.length - 1));
  497. result.push(ignore + '**');
  498. } else if (!ignore.endsWith('*')) {
  499. result.push(ignore + '/**');
  500. }
  501. }
  502. });
  503. }
  504. }
  505. // store result
  506. cache.ignores = result;
  507. dataCache.set(this, cache);
  508. return result;
  509. }
  510. /**
  511. * Indicates if the config array has been normalized.
  512. * @returns {boolean} True if the config array is normalized, false if not.
  513. */
  514. isNormalized() {
  515. return this[ConfigArraySymbol.isNormalized];
  516. }
  517. /**
  518. * Normalizes a config array by flattening embedded arrays and executing
  519. * config functions.
  520. * @param {ConfigContext} context The context object for config functions.
  521. * @returns {Promise<ConfigArray>} The current ConfigArray instance.
  522. */
  523. async normalize(context = {}) {
  524. if (!this.isNormalized()) {
  525. const normalizedConfigs = await normalize(this, context, this.extraConfigTypes);
  526. this.length = 0;
  527. this.push(...normalizedConfigs.map(this[ConfigArraySymbol.preprocessConfig].bind(this)));
  528. this[ConfigArraySymbol.isNormalized] = true;
  529. // prevent further changes
  530. Object.freeze(this);
  531. }
  532. return this;
  533. }
  534. /**
  535. * Normalizes a config array by flattening embedded arrays and executing
  536. * config functions.
  537. * @param {ConfigContext} context The context object for config functions.
  538. * @returns {ConfigArray} The current ConfigArray instance.
  539. */
  540. normalizeSync(context = {}) {
  541. if (!this.isNormalized()) {
  542. const normalizedConfigs = normalizeSync(this, context, this.extraConfigTypes);
  543. this.length = 0;
  544. this.push(...normalizedConfigs.map(this[ConfigArraySymbol.preprocessConfig].bind(this)));
  545. this[ConfigArraySymbol.isNormalized] = true;
  546. // prevent further changes
  547. Object.freeze(this);
  548. }
  549. return this;
  550. }
  551. /**
  552. * Finalizes the state of a config before being cached and returned by
  553. * `getConfig()`. Does nothing by default but is provided to be
  554. * overridden by subclasses as necessary.
  555. * @param {Object} config The config to finalize.
  556. * @returns {Object} The finalized config.
  557. */
  558. [ConfigArraySymbol.finalizeConfig](config) {
  559. return config;
  560. }
  561. /**
  562. * Preprocesses a config during the normalization process. This is the
  563. * method to override if you want to convert an array item before it is
  564. * validated for the first time. For example, if you want to replace a
  565. * string with an object, this is the method to override.
  566. * @param {Object} config The config to preprocess.
  567. * @returns {Object} The config to use in place of the argument.
  568. */
  569. [ConfigArraySymbol.preprocessConfig](config) {
  570. return config;
  571. }
  572. /**
  573. * Determines if a given file path explicitly matches a `files` entry
  574. * and also doesn't match an `ignores` entry. Configs that don't have
  575. * a `files` property are not considered an explicit match.
  576. * @param {string} filePath The complete path of a file to check.
  577. * @returns {boolean} True if the file path matches a `files` entry
  578. * or false if not.
  579. */
  580. isExplicitMatch(filePath) {
  581. assertNormalized(this);
  582. const cache = dataCache.get(this);
  583. // first check the cache to avoid duplicate work
  584. let result = cache.explicitMatches.get(filePath);
  585. if (typeof result == 'boolean') {
  586. return result;
  587. }
  588. // TODO: Maybe move elsewhere? Maybe combine with getConfig() logic?
  589. const relativeFilePath = path.relative(this.basePath, filePath);
  590. if (shouldIgnorePath(this.ignores, filePath, relativeFilePath)) {
  591. debug(`Ignoring ${filePath}`);
  592. // cache and return result
  593. cache.explicitMatches.set(filePath, false);
  594. return false;
  595. }
  596. // filePath isn't automatically ignored, so try to find a match
  597. for (const config of this) {
  598. if (!config.files) {
  599. continue;
  600. }
  601. if (pathMatches(filePath, this.basePath, config)) {
  602. debug(`Matching config found for ${filePath}`);
  603. cache.explicitMatches.set(filePath, true);
  604. return true;
  605. }
  606. }
  607. return false;
  608. }
  609. /**
  610. * Returns the config object for a given file path.
  611. * @param {string} filePath The complete path of a file to get a config for.
  612. * @returns {Object} The config object for this file.
  613. */
  614. getConfig(filePath) {
  615. assertNormalized(this);
  616. const cache = this[ConfigArraySymbol.configCache];
  617. // first check the cache for a filename match to avoid duplicate work
  618. let finalConfig = cache.get(filePath);
  619. if (finalConfig) {
  620. return finalConfig;
  621. }
  622. // next check to see if the file should be ignored
  623. // check if this should be ignored due to its directory
  624. if (this.isDirectoryIgnored(path.dirname(filePath))) {
  625. debug(`Ignoring ${filePath} based on directory pattern`);
  626. // cache and return result - finalConfig is undefined at this point
  627. cache.set(filePath, finalConfig);
  628. return finalConfig;
  629. }
  630. // TODO: Maybe move elsewhere?
  631. const relativeFilePath = path.relative(this.basePath, filePath);
  632. if (shouldIgnorePath(this.ignores, filePath, relativeFilePath)) {
  633. debug(`Ignoring ${filePath} based on file pattern`);
  634. // cache and return result - finalConfig is undefined at this point
  635. cache.set(filePath, finalConfig);
  636. return finalConfig;
  637. }
  638. // filePath isn't automatically ignored, so try to construct config
  639. const matchingConfigIndices = [];
  640. let matchFound = false;
  641. const universalPattern = /\/\*{1,2}$/;
  642. this.forEach((config, index) => {
  643. if (!config.files) {
  644. debug(`Anonymous universal config found for ${filePath}`);
  645. matchingConfigIndices.push(index);
  646. return;
  647. }
  648. assertNonEmptyFilesArray(config);
  649. /*
  650. * If a config has a files pattern ending in /** or /*, and the
  651. * filePath only matches those patterns, then the config is only
  652. * applied if there is another config where the filePath matches
  653. * a file with a specific extensions such as *.js.
  654. */
  655. const universalFiles = config.files.filter(
  656. pattern => universalPattern.test(pattern)
  657. );
  658. // universal patterns were found so we need to check the config twice
  659. if (universalFiles.length) {
  660. debug('Universal files patterns found. Checking carefully.');
  661. const nonUniversalFiles = config.files.filter(
  662. pattern => !universalPattern.test(pattern)
  663. );
  664. // check that the config matches without the non-universal files first
  665. if (
  666. nonUniversalFiles.length &&
  667. pathMatches(
  668. filePath, this.basePath,
  669. { files: nonUniversalFiles, ignores: config.ignores }
  670. )
  671. ) {
  672. debug(`Matching config found for ${filePath}`);
  673. matchingConfigIndices.push(index);
  674. matchFound = true;
  675. return;
  676. }
  677. // if there wasn't a match then check if it matches with universal files
  678. if (
  679. universalFiles.length &&
  680. pathMatches(
  681. filePath, this.basePath,
  682. { files: universalFiles, ignores: config.ignores }
  683. )
  684. ) {
  685. debug(`Matching config found for ${filePath}`);
  686. matchingConfigIndices.push(index);
  687. return;
  688. }
  689. // if we make here, then there was no match
  690. return;
  691. }
  692. // the normal case
  693. if (pathMatches(filePath, this.basePath, config)) {
  694. debug(`Matching config found for ${filePath}`);
  695. matchingConfigIndices.push(index);
  696. matchFound = true;
  697. return;
  698. }
  699. });
  700. // if matching both files and ignores, there will be no config to create
  701. if (!matchFound) {
  702. debug(`No matching configs found for ${filePath}`);
  703. // cache and return result - finalConfig is undefined at this point
  704. cache.set(filePath, finalConfig);
  705. return finalConfig;
  706. }
  707. // check to see if there is a config cached by indices
  708. finalConfig = cache.get(matchingConfigIndices.toString());
  709. if (finalConfig) {
  710. // also store for filename for faster lookup next time
  711. cache.set(filePath, finalConfig);
  712. return finalConfig;
  713. }
  714. // otherwise construct the config
  715. finalConfig = matchingConfigIndices.reduce((result, index) => {
  716. return this[ConfigArraySymbol.schema].merge(result, this[index]);
  717. }, {}, this);
  718. finalConfig = this[ConfigArraySymbol.finalizeConfig](finalConfig);
  719. cache.set(filePath, finalConfig);
  720. cache.set(matchingConfigIndices.toString(), finalConfig);
  721. return finalConfig;
  722. }
  723. /**
  724. * Determines if the given filepath is ignored based on the configs.
  725. * @param {string} filePath The complete path of a file to check.
  726. * @returns {boolean} True if the path is ignored, false if not.
  727. * @deprecated Use `isFileIgnored` instead.
  728. */
  729. isIgnored(filePath) {
  730. return this.isFileIgnored(filePath);
  731. }
  732. /**
  733. * Determines if the given filepath is ignored based on the configs.
  734. * @param {string} filePath The complete path of a file to check.
  735. * @returns {boolean} True if the path is ignored, false if not.
  736. */
  737. isFileIgnored(filePath) {
  738. return this.getConfig(filePath) === undefined;
  739. }
  740. /**
  741. * Determines if the given directory is ignored based on the configs.
  742. * This checks only default `ignores` that don't have `files` in the
  743. * same config. A pattern such as `/foo` be considered to ignore the directory
  744. * while a pattern such as `/foo/**` is not considered to ignore the
  745. * directory because it is matching files.
  746. * @param {string} directoryPath The complete path of a directory to check.
  747. * @returns {boolean} True if the directory is ignored, false if not. Will
  748. * return true for any directory that is not inside of `basePath`.
  749. * @throws {Error} When the `ConfigArray` is not normalized.
  750. */
  751. isDirectoryIgnored(directoryPath) {
  752. assertNormalized(this);
  753. const relativeDirectoryPath = path.relative(this.basePath, directoryPath)
  754. .replace(/\\/g, '/');
  755. if (relativeDirectoryPath.startsWith('..')) {
  756. return true;
  757. }
  758. // first check the cache
  759. const cache = dataCache.get(this).directoryMatches;
  760. if (cache.has(relativeDirectoryPath)) {
  761. return cache.get(relativeDirectoryPath);
  762. }
  763. const directoryParts = relativeDirectoryPath.split('/');
  764. let relativeDirectoryToCheck = '';
  765. let result = false;
  766. /*
  767. * In order to get the correct gitignore-style ignores, where an
  768. * ignored parent directory cannot have any descendants unignored,
  769. * we need to check every directory starting at the parent all
  770. * the way down to the actual requested directory.
  771. *
  772. * We aggressively cache all of this info to make sure we don't
  773. * have to recalculate everything for every call.
  774. */
  775. do {
  776. relativeDirectoryToCheck += directoryParts.shift() + '/';
  777. result = shouldIgnorePath(
  778. this.ignores,
  779. path.join(this.basePath, relativeDirectoryToCheck),
  780. relativeDirectoryToCheck
  781. );
  782. cache.set(relativeDirectoryToCheck, result);
  783. } while (!result && directoryParts.length);
  784. // also cache the result for the requested path
  785. cache.set(relativeDirectoryPath, result);
  786. return result;
  787. }
  788. }
  789. exports.ConfigArray = ConfigArray;
  790. exports.ConfigArraySymbol = ConfigArraySymbol;