api.js 27 KB

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