租房小程序前端代码
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1128 lines
30 KiB

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