Spaces:
Paused
Paused
| ; | |
| var posixPath = require('./std__path/posix.cjs'); | |
| var windowsPath = require('./std__path/windows.cjs'); | |
| var minimatch = require('minimatch'); | |
| var createDebug = require('debug'); | |
| var objectSchema = require('@eslint/object-schema'); | |
| function _interopNamespaceDefault(e) { | |
| var n = Object.create(null); | |
| if (e) { | |
| Object.keys(e).forEach(function (k) { | |
| if (k !== 'default') { | |
| var d = Object.getOwnPropertyDescriptor(e, k); | |
| Object.defineProperty(n, k, d.get ? d : { | |
| enumerable: true, | |
| get: function () { return e[k]; } | |
| }); | |
| } | |
| }); | |
| } | |
| n.default = e; | |
| return Object.freeze(n); | |
| } | |
| var posixPath__namespace = /*#__PURE__*/_interopNamespaceDefault(posixPath); | |
| var windowsPath__namespace = /*#__PURE__*/_interopNamespaceDefault(windowsPath); | |
| /** | |
| * @fileoverview ConfigSchema | |
| * @author Nicholas C. Zakas | |
| */ | |
| //------------------------------------------------------------------------------ | |
| // Types | |
| //------------------------------------------------------------------------------ | |
| /** @typedef {import("@eslint/object-schema").PropertyDefinition} PropertyDefinition */ | |
| /** @typedef {import("@eslint/object-schema").ObjectDefinition} ObjectDefinition */ | |
| //------------------------------------------------------------------------------ | |
| // Helpers | |
| //------------------------------------------------------------------------------ | |
| /** | |
| * A strategy that does nothing. | |
| * @type {PropertyDefinition} | |
| */ | |
| const NOOP_STRATEGY = { | |
| required: false, | |
| merge() { | |
| return undefined; | |
| }, | |
| validate() {}, | |
| }; | |
| //------------------------------------------------------------------------------ | |
| // Exports | |
| //------------------------------------------------------------------------------ | |
| /** | |
| * The base schema that every ConfigArray uses. | |
| * @type {ObjectDefinition} | |
| */ | |
| const baseSchema = Object.freeze({ | |
| name: { | |
| required: false, | |
| merge() { | |
| return undefined; | |
| }, | |
| validate(value) { | |
| if (typeof value !== "string") { | |
| throw new TypeError("Property must be a string."); | |
| } | |
| }, | |
| }, | |
| files: NOOP_STRATEGY, | |
| ignores: NOOP_STRATEGY, | |
| }); | |
| /** | |
| * @fileoverview ConfigSchema | |
| * @author Nicholas C. Zakas | |
| */ | |
| //------------------------------------------------------------------------------ | |
| // Types | |
| //------------------------------------------------------------------------------ | |
| //------------------------------------------------------------------------------ | |
| // Helpers | |
| //------------------------------------------------------------------------------ | |
| /** | |
| * Asserts that a given value is an array. | |
| * @param {*} value The value to check. | |
| * @returns {void} | |
| * @throws {TypeError} When the value is not an array. | |
| */ | |
| function assertIsArray(value) { | |
| if (!Array.isArray(value)) { | |
| throw new TypeError("Expected value to be an array."); | |
| } | |
| } | |
| /** | |
| * Asserts that a given value is an array containing only strings and functions. | |
| * @param {*} value The value to check. | |
| * @returns {void} | |
| * @throws {TypeError} When the value is not an array of strings and functions. | |
| */ | |
| function assertIsArrayOfStringsAndFunctions(value) { | |
| assertIsArray(value); | |
| if ( | |
| value.some( | |
| item => typeof item !== "string" && typeof item !== "function", | |
| ) | |
| ) { | |
| throw new TypeError( | |
| "Expected array to only contain strings and functions.", | |
| ); | |
| } | |
| } | |
| /** | |
| * Asserts that a given value is a non-empty array. | |
| * @param {*} value The value to check. | |
| * @returns {void} | |
| * @throws {TypeError} When the value is not an array or an empty array. | |
| */ | |
| function assertIsNonEmptyArray(value) { | |
| if (!Array.isArray(value) || value.length === 0) { | |
| throw new TypeError("Expected value to be a non-empty array."); | |
| } | |
| } | |
| //------------------------------------------------------------------------------ | |
| // Exports | |
| //------------------------------------------------------------------------------ | |
| /** | |
| * The schema for `files` and `ignores` that every ConfigArray uses. | |
| * @type {ObjectDefinition} | |
| */ | |
| const filesAndIgnoresSchema = Object.freeze({ | |
| files: { | |
| required: false, | |
| merge() { | |
| return undefined; | |
| }, | |
| validate(value) { | |
| // first check if it's an array | |
| assertIsNonEmptyArray(value); | |
| // then check each member | |
| value.forEach(item => { | |
| if (Array.isArray(item)) { | |
| assertIsArrayOfStringsAndFunctions(item); | |
| } else if ( | |
| typeof item !== "string" && | |
| typeof item !== "function" | |
| ) { | |
| throw new TypeError( | |
| "Items must be a string, a function, or an array of strings and functions.", | |
| ); | |
| } | |
| }); | |
| }, | |
| }, | |
| ignores: { | |
| required: false, | |
| merge() { | |
| return undefined; | |
| }, | |
| validate: assertIsArrayOfStringsAndFunctions, | |
| }, | |
| }); | |
| /** | |
| * @fileoverview ConfigArray | |
| * @author Nicholas C. Zakas | |
| */ | |
| //------------------------------------------------------------------------------ | |
| // Types | |
| //------------------------------------------------------------------------------ | |
| /** @typedef {import("./types.ts").ConfigObject} ConfigObject */ | |
| /** @typedef {import("minimatch").IMinimatchStatic} IMinimatchStatic */ | |
| /** @typedef {import("minimatch").IMinimatch} IMinimatch */ | |
| /** @typedef {import("@jsr/std__path")} PathImpl */ | |
| /* | |
| * This is a bit of a hack to make TypeScript happy with the Rollup-created | |
| * CommonJS file. Rollup doesn't do object destructuring for imported files | |
| * and instead imports the default via `require()`. This messes up type checking | |
| * for `ObjectSchema`. To work around that, we just import the type manually | |
| * and give it a different name to use in the JSDoc comments. | |
| */ | |
| /** @typedef {import("@eslint/object-schema").ObjectSchema} ObjectSchemaInstance */ | |
| //------------------------------------------------------------------------------ | |
| // Helpers | |
| //------------------------------------------------------------------------------ | |
| const Minimatch = minimatch.Minimatch; | |
| const debug = createDebug("@eslint/config-array"); | |
| /** | |
| * A cache for minimatch instances. | |
| * @type {Map<string, IMinimatch>} | |
| */ | |
| const minimatchCache = new Map(); | |
| /** | |
| * A cache for negated minimatch instances. | |
| * @type {Map<string, IMinimatch>} | |
| */ | |
| const negatedMinimatchCache = new Map(); | |
| /** | |
| * Options to use with minimatch. | |
| * @type {Object} | |
| */ | |
| const MINIMATCH_OPTIONS = { | |
| // matchBase: true, | |
| dot: true, | |
| allowWindowsEscape: true, | |
| }; | |
| /** | |
| * The types of config objects that are supported. | |
| * @type {Set<string>} | |
| */ | |
| const CONFIG_TYPES = new Set(["array", "function"]); | |
| /** | |
| * Fields that are considered metadata and not part of the config object. | |
| * @type {Set<string>} | |
| */ | |
| const META_FIELDS = new Set(["name"]); | |
| /** | |
| * A schema containing just files and ignores for early validation. | |
| * @type {ObjectSchemaInstance} | |
| */ | |
| const FILES_AND_IGNORES_SCHEMA = new objectSchema.ObjectSchema(filesAndIgnoresSchema); | |
| // Precomputed constant objects returned by `ConfigArray.getConfigWithStatus`. | |
| const CONFIG_WITH_STATUS_EXTERNAL = Object.freeze({ status: "external" }); | |
| const CONFIG_WITH_STATUS_IGNORED = Object.freeze({ status: "ignored" }); | |
| const CONFIG_WITH_STATUS_UNCONFIGURED = Object.freeze({ | |
| status: "unconfigured", | |
| }); | |
| // Match two leading dots followed by a slash or the end of input. | |
| const EXTERNAL_PATH_REGEX = /^\.\.(?:\/|$)/u; | |
| /** | |
| * Wrapper error for config validation errors that adds a name to the front of the | |
| * error message. | |
| */ | |
| class ConfigError extends Error { | |
| /** | |
| * Creates a new instance. | |
| * @param {string} name The config object name causing the error. | |
| * @param {number} index The index of the config object in the array. | |
| * @param {Object} options The options for the error. | |
| * @param {Error} [options.cause] The error that caused this error. | |
| * @param {string} [options.message] The message to use for the error. | |
| */ | |
| constructor(name, index, { cause, message }) { | |
| const finalMessage = message || cause.message; | |
| super(`Config ${name}: ${finalMessage}`, { cause }); | |
| // copy over custom properties that aren't represented | |
| if (cause) { | |
| for (const key of Object.keys(cause)) { | |
| if (!(key in this)) { | |
| this[key] = cause[key]; | |
| } | |
| } | |
| } | |
| /** | |
| * The name of the error. | |
| * @type {string} | |
| * @readonly | |
| */ | |
| this.name = "ConfigError"; | |
| /** | |
| * The index of the config object in the array. | |
| * @type {number} | |
| * @readonly | |
| */ | |
| this.index = index; | |
| } | |
| } | |
| /** | |
| * Gets the name of a config object. | |
| * @param {ConfigObject} config The config object to get the name of. | |
| * @returns {string} The name of the config object. | |
| */ | |
| function getConfigName(config) { | |
| if (config && typeof config.name === "string" && config.name) { | |
| return `"${config.name}"`; | |
| } | |
| return "(unnamed)"; | |
| } | |
| /** | |
| * Rethrows a config error with additional information about the config object. | |
| * @param {object} config The config object to get the name of. | |
| * @param {number} index The index of the config object in the array. | |
| * @param {Error} error The error to rethrow. | |
| * @throws {ConfigError} When the error is rethrown for a config. | |
| */ | |
| function rethrowConfigError(config, index, error) { | |
| const configName = getConfigName(config); | |
| throw new ConfigError(configName, index, { cause: error }); | |
| } | |
| /** | |
| * Shorthand for checking if a value is a string. | |
| * @param {any} value The value to check. | |
| * @returns {boolean} True if a string, false if not. | |
| */ | |
| function isString(value) { | |
| return typeof value === "string"; | |
| } | |
| /** | |
| * Creates a function that asserts that the config is valid | |
| * during normalization. This checks that the config is not nullish | |
| * and that files and ignores keys of a config object are valid as per base schema. | |
| * @param {Object} config The config object to check. | |
| * @param {number} index The index of the config object in the array. | |
| * @returns {void} | |
| * @throws {ConfigError} If the files and ignores keys of a config object are not valid. | |
| */ | |
| function assertValidBaseConfig(config, index) { | |
| if (config === null) { | |
| throw new ConfigError(getConfigName(config), index, { | |
| message: "Unexpected null config.", | |
| }); | |
| } | |
| if (config === undefined) { | |
| throw new ConfigError(getConfigName(config), index, { | |
| message: "Unexpected undefined config.", | |
| }); | |
| } | |
| if (typeof config !== "object") { | |
| throw new ConfigError(getConfigName(config), index, { | |
| message: "Unexpected non-object config.", | |
| }); | |
| } | |
| const validateConfig = {}; | |
| if ("files" in config) { | |
| validateConfig.files = config.files; | |
| } | |
| if ("ignores" in config) { | |
| validateConfig.ignores = config.ignores; | |
| } | |
| try { | |
| FILES_AND_IGNORES_SCHEMA.validate(validateConfig); | |
| } catch (validationError) { | |
| rethrowConfigError(config, index, validationError); | |
| } | |
| } | |
| /** | |
| * Wrapper around minimatch that caches minimatch patterns for | |
| * faster matching speed over multiple file path evaluations. | |
| * @param {string} filepath The file path to match. | |
| * @param {string} pattern The glob pattern to match against. | |
| * @param {object} options The minimatch options to use. | |
| * @returns | |
| */ | |
| function doMatch(filepath, pattern, options = {}) { | |
| let cache = minimatchCache; | |
| if (options.flipNegate) { | |
| cache = negatedMinimatchCache; | |
| } | |
| let matcher = cache.get(pattern); | |
| if (!matcher) { | |
| matcher = new Minimatch( | |
| pattern, | |
| Object.assign({}, MINIMATCH_OPTIONS, options), | |
| ); | |
| cache.set(pattern, matcher); | |
| } | |
| return matcher.match(filepath); | |
| } | |
| /** | |
| * Normalizes a `ConfigArray` by flattening it and executing any functions | |
| * that are found inside. | |
| * @param {Array} items The items in a `ConfigArray`. | |
| * @param {Object} context The context object to pass into any function | |
| * found. | |
| * @param {Array<string>} extraConfigTypes The config types to check. | |
| * @returns {Promise<Array>} A flattened array containing only config objects. | |
| * @throws {TypeError} When a config function returns a function. | |
| */ | |
| async function normalize(items, context, extraConfigTypes) { | |
| const allowFunctions = extraConfigTypes.includes("function"); | |
| const allowArrays = extraConfigTypes.includes("array"); | |
| async function* flatTraverse(array) { | |
| for (let item of array) { | |
| if (typeof item === "function") { | |
| if (!allowFunctions) { | |
| throw new TypeError("Unexpected function."); | |
| } | |
| item = item(context); | |
| if (item.then) { | |
| item = await item; | |
| } | |
| } | |
| if (Array.isArray(item)) { | |
| if (!allowArrays) { | |
| throw new TypeError("Unexpected array."); | |
| } | |
| yield* flatTraverse(item); | |
| } else if (typeof item === "function") { | |
| throw new TypeError( | |
| "A config function can only return an object or array.", | |
| ); | |
| } else { | |
| yield item; | |
| } | |
| } | |
| } | |
| /* | |
| * Async iterables cannot be used with the spread operator, so we need to manually | |
| * create the array to return. | |
| */ | |
| const asyncIterable = await flatTraverse(items); | |
| const configs = []; | |
| for await (const config of asyncIterable) { | |
| configs.push(config); | |
| } | |
| return configs; | |
| } | |
| /** | |
| * Normalizes a `ConfigArray` by flattening it and executing any functions | |
| * that are found inside. | |
| * @param {Array} items The items in a `ConfigArray`. | |
| * @param {Object} context The context object to pass into any function | |
| * found. | |
| * @param {Array<string>} extraConfigTypes The config types to check. | |
| * @returns {Array} A flattened array containing only config objects. | |
| * @throws {TypeError} When a config function returns a function. | |
| */ | |
| function normalizeSync(items, context, extraConfigTypes) { | |
| const allowFunctions = extraConfigTypes.includes("function"); | |
| const allowArrays = extraConfigTypes.includes("array"); | |
| function* flatTraverse(array) { | |
| for (let item of array) { | |
| if (typeof item === "function") { | |
| if (!allowFunctions) { | |
| throw new TypeError("Unexpected function."); | |
| } | |
| item = item(context); | |
| if (item.then) { | |
| throw new TypeError( | |
| "Async config functions are not supported.", | |
| ); | |
| } | |
| } | |
| if (Array.isArray(item)) { | |
| if (!allowArrays) { | |
| throw new TypeError("Unexpected array."); | |
| } | |
| yield* flatTraverse(item); | |
| } else if (typeof item === "function") { | |
| throw new TypeError( | |
| "A config function can only return an object or array.", | |
| ); | |
| } else { | |
| yield item; | |
| } | |
| } | |
| } | |
| return [...flatTraverse(items)]; | |
| } | |
| /** | |
| * Determines if a given file path should be ignored based on the given | |
| * matcher. | |
| * @param {Array<string|((string) => boolean)>} ignores The ignore patterns to check. | |
| * @param {string} filePath The unprocessed file path to check. | |
| * @param {string} relativeFilePath The path of the file to check relative to the base path, | |
| * using forward slash (`"/"`) as a separator. | |
| * @returns {boolean} True if the path should be ignored and false if not. | |
| */ | |
| function shouldIgnorePath(ignores, filePath, relativeFilePath) { | |
| return ignores.reduce((ignored, matcher) => { | |
| if (!ignored) { | |
| if (typeof matcher === "function") { | |
| return matcher(filePath); | |
| } | |
| // don't check negated patterns because we're not ignored yet | |
| if (!matcher.startsWith("!")) { | |
| return doMatch(relativeFilePath, matcher); | |
| } | |
| // otherwise we're still not ignored | |
| return false; | |
| } | |
| // only need to check negated patterns because we're ignored | |
| if (typeof matcher === "string" && matcher.startsWith("!")) { | |
| return !doMatch(relativeFilePath, matcher, { | |
| flipNegate: true, | |
| }); | |
| } | |
| return ignored; | |
| }, false); | |
| } | |
| /** | |
| * Determines if a given file path is matched by a config based on | |
| * `ignores` only. | |
| * @param {string} filePath The unprocessed file path to check. | |
| * @param {string} relativeFilePath The path of the file to check relative to the base path, | |
| * using forward slash (`"/"`) as a separator. | |
| * @param {Object} config The config object to check. | |
| * @returns {boolean} True if the file path is matched by the config, | |
| * false if not. | |
| */ | |
| function pathMatchesIgnores(filePath, relativeFilePath, config) { | |
| return ( | |
| Object.keys(config).filter(key => !META_FIELDS.has(key)).length > 1 && | |
| !shouldIgnorePath(config.ignores, filePath, relativeFilePath) | |
| ); | |
| } | |
| /** | |
| * Determines if a given file path is matched by a config. If the config | |
| * has no `files` field, then it matches; otherwise, if a `files` field | |
| * is present then we match the globs in `files` and exclude any globs in | |
| * `ignores`. | |
| * @param {string} filePath The unprocessed file path to check. | |
| * @param {string} relativeFilePath The path of the file to check relative to the base path, | |
| * using forward slash (`"/"`) as a separator. | |
| * @param {Object} config The config object to check. | |
| * @returns {boolean} True if the file path is matched by the config, | |
| * false if not. | |
| */ | |
| function pathMatches(filePath, relativeFilePath, config) { | |
| // match both strings and functions | |
| function match(pattern) { | |
| if (isString(pattern)) { | |
| return doMatch(relativeFilePath, pattern); | |
| } | |
| if (typeof pattern === "function") { | |
| return pattern(filePath); | |
| } | |
| throw new TypeError(`Unexpected matcher type ${pattern}.`); | |
| } | |
| // check for all matches to config.files | |
| let filePathMatchesPattern = config.files.some(pattern => { | |
| if (Array.isArray(pattern)) { | |
| return pattern.every(match); | |
| } | |
| return match(pattern); | |
| }); | |
| /* | |
| * If the file path matches the config.files patterns, then check to see | |
| * if there are any files to ignore. | |
| */ | |
| if (filePathMatchesPattern && config.ignores) { | |
| filePathMatchesPattern = !shouldIgnorePath( | |
| config.ignores, | |
| filePath, | |
| relativeFilePath, | |
| ); | |
| } | |
| return filePathMatchesPattern; | |
| } | |
| /** | |
| * Ensures that a ConfigArray has been normalized. | |
| * @param {ConfigArray} configArray The ConfigArray to check. | |
| * @returns {void} | |
| * @throws {Error} When the `ConfigArray` is not normalized. | |
| */ | |
| function assertNormalized(configArray) { | |
| // TODO: Throw more verbose error | |
| if (!configArray.isNormalized()) { | |
| throw new Error( | |
| "ConfigArray must be normalized to perform this operation.", | |
| ); | |
| } | |
| } | |
| /** | |
| * Ensures that config types are valid. | |
| * @param {Array<string>} extraConfigTypes The config types to check. | |
| * @returns {void} | |
| * @throws {Error} When the config types array is invalid. | |
| */ | |
| function assertExtraConfigTypes(extraConfigTypes) { | |
| if (extraConfigTypes.length > 2) { | |
| throw new TypeError( | |
| "configTypes must be an array with at most two items.", | |
| ); | |
| } | |
| for (const configType of extraConfigTypes) { | |
| if (!CONFIG_TYPES.has(configType)) { | |
| throw new TypeError( | |
| `Unexpected config type "${configType}" found. Expected one of: "object", "array", "function".`, | |
| ); | |
| } | |
| } | |
| } | |
| /** | |
| * Returns path-handling implementations for Unix or Windows, depending on a given absolute path. | |
| * @param {string} fileOrDirPath The absolute path to check. | |
| * @returns {PathImpl} Path-handling implementations for the specified path. | |
| * @throws An error is thrown if the specified argument is not an absolute path. | |
| */ | |
| function getPathImpl(fileOrDirPath) { | |
| // Posix absolute paths always start with a slash. | |
| if (fileOrDirPath.startsWith("/")) { | |
| return posixPath__namespace; | |
| } | |
| // Windows absolute paths start with a letter followed by a colon and at least one backslash, | |
| // or with two backslashes in the case of UNC paths. | |
| // Forward slashed are automatically normalized to backslashes. | |
| if (/^(?:[A-Za-z]:[/\\]|[/\\]{2})/u.test(fileOrDirPath)) { | |
| return windowsPath__namespace; | |
| } | |
| throw new Error( | |
| `Expected an absolute path but received "${fileOrDirPath}"`, | |
| ); | |
| } | |
| /** | |
| * Converts a given path to a relative path with all separator characters replaced by forward slashes (`"/"`). | |
| * @param {string} fileOrDirPath The unprocessed path to convert. | |
| * @param {string} namespacedBasePath The namespaced base path of the directory to which the calculated path shall be relative. | |
| * @param {PathImpl} path Path-handling implementations. | |
| * @returns {string} A relative path with all separator characters replaced by forward slashes. | |
| */ | |
| function toRelativePath(fileOrDirPath, namespacedBasePath, path) { | |
| const fullPath = path.resolve(namespacedBasePath, fileOrDirPath); | |
| const namespacedFullPath = path.toNamespacedPath(fullPath); | |
| const relativePath = path.relative(namespacedBasePath, namespacedFullPath); | |
| return relativePath.replaceAll(path.SEPARATOR, "/"); | |
| } | |
| //------------------------------------------------------------------------------ | |
| // Public Interface | |
| //------------------------------------------------------------------------------ | |
| const ConfigArraySymbol = { | |
| isNormalized: Symbol("isNormalized"), | |
| configCache: Symbol("configCache"), | |
| schema: Symbol("schema"), | |
| finalizeConfig: Symbol("finalizeConfig"), | |
| preprocessConfig: Symbol("preprocessConfig"), | |
| }; | |
| // used to store calculate data for faster lookup | |
| const dataCache = new WeakMap(); | |
| /** | |
| * Represents an array of config objects and provides method for working with | |
| * those config objects. | |
| */ | |
| class ConfigArray extends Array { | |
| /** | |
| * The namespaced path of the config file directory. | |
| * @type {string} | |
| */ | |
| #namespacedBasePath; | |
| /** | |
| * Path-handling implementations. | |
| * @type {PathImpl} | |
| */ | |
| #path; | |
| /** | |
| * Creates a new instance of ConfigArray. | |
| * @param {Iterable|Function|Object} configs An iterable yielding config | |
| * objects, or a config function, or a config object. | |
| * @param {Object} options The options for the ConfigArray. | |
| * @param {string} [options.basePath="/"] The absolute path of the config file directory. | |
| * Defaults to `"/"`. | |
| * @param {boolean} [options.normalized=false] Flag indicating if the | |
| * configs have already been normalized. | |
| * @param {Object} [options.schema] The additional schema | |
| * definitions to use for the ConfigArray schema. | |
| * @param {Array<string>} [options.extraConfigTypes] List of config types supported. | |
| */ | |
| constructor( | |
| configs, | |
| { | |
| basePath = "/", | |
| normalized = false, | |
| schema: customSchema, | |
| extraConfigTypes = [], | |
| } = {}, | |
| ) { | |
| super(); | |
| /** | |
| * Tracks if the array has been normalized. | |
| * @property isNormalized | |
| * @type {boolean} | |
| * @private | |
| */ | |
| this[ConfigArraySymbol.isNormalized] = normalized; | |
| /** | |
| * The schema used for validating and merging configs. | |
| * @property schema | |
| * @type {ObjectSchemaInstance} | |
| * @private | |
| */ | |
| this[ConfigArraySymbol.schema] = new objectSchema.ObjectSchema( | |
| Object.assign({}, customSchema, baseSchema), | |
| ); | |
| if (!isString(basePath) || !basePath) { | |
| throw new TypeError("basePath must be a non-empty string"); | |
| } | |
| /** | |
| * The path of the config file that this array was loaded from. | |
| * This is used to calculate filename matches. | |
| * @property basePath | |
| * @type {string} | |
| */ | |
| this.basePath = basePath; | |
| assertExtraConfigTypes(extraConfigTypes); | |
| /** | |
| * The supported config types. | |
| * @type {Array<string>} | |
| */ | |
| this.extraConfigTypes = [...extraConfigTypes]; | |
| Object.freeze(this.extraConfigTypes); | |
| /** | |
| * A cache to store calculated configs for faster repeat lookup. | |
| * @property configCache | |
| * @type {Map<string, Object>} | |
| * @private | |
| */ | |
| this[ConfigArraySymbol.configCache] = new Map(); | |
| // init cache | |
| dataCache.set(this, { | |
| explicitMatches: new Map(), | |
| directoryMatches: new Map(), | |
| files: undefined, | |
| ignores: undefined, | |
| }); | |
| // load the configs into this array | |
| if (Array.isArray(configs)) { | |
| this.push(...configs); | |
| } else { | |
| this.push(configs); | |
| } | |
| // select path-handling implementations depending on the base path | |
| this.#path = getPathImpl(basePath); | |
| // On Windows, `path.relative()` returns an absolute path when given two paths on different drives. | |
| // The namespaced base path is useful to make sure that calculated relative paths are always relative. | |
| // On Unix, it is identical to the base path. | |
| this.#namespacedBasePath = this.#path.toNamespacedPath(basePath); | |
| } | |
| /** | |
| * Prevent normal array methods from creating a new `ConfigArray` instance. | |
| * This is to ensure that methods such as `slice()` won't try to create a | |
| * new instance of `ConfigArray` behind the scenes as doing so may throw | |
| * an error due to the different constructor signature. | |
| * @type {ArrayConstructor} The `Array` constructor. | |
| */ | |
| static get [Symbol.species]() { | |
| return Array; | |
| } | |
| /** | |
| * Returns the `files` globs from every config object in the array. | |
| * This can be used to determine which files will be matched by a | |
| * config array or to use as a glob pattern when no patterns are provided | |
| * for a command line interface. | |
| * @returns {Array<string|Function>} An array of matchers. | |
| */ | |
| get files() { | |
| assertNormalized(this); | |
| // if this data has been cached, retrieve it | |
| const cache = dataCache.get(this); | |
| if (cache.files) { | |
| return cache.files; | |
| } | |
| // otherwise calculate it | |
| const result = []; | |
| for (const config of this) { | |
| if (config.files) { | |
| config.files.forEach(filePattern => { | |
| result.push(filePattern); | |
| }); | |
| } | |
| } | |
| // store result | |
| cache.files = result; | |
| dataCache.set(this, cache); | |
| return result; | |
| } | |
| /** | |
| * Returns ignore matchers that should always be ignored regardless of | |
| * the matching `files` fields in any configs. This is necessary to mimic | |
| * the behavior of things like .gitignore and .eslintignore, allowing a | |
| * globbing operation to be faster. | |
| * @returns {string[]} An array of string patterns and functions to be ignored. | |
| */ | |
| get ignores() { | |
| assertNormalized(this); | |
| // if this data has been cached, retrieve it | |
| const cache = dataCache.get(this); | |
| if (cache.ignores) { | |
| return cache.ignores; | |
| } | |
| // otherwise calculate it | |
| const result = []; | |
| for (const config of this) { | |
| /* | |
| * We only count ignores if there are no other keys in the object. | |
| * In this case, it acts list a globally ignored pattern. If there | |
| * are additional keys, then ignores act like exclusions. | |
| */ | |
| if ( | |
| config.ignores && | |
| Object.keys(config).filter(key => !META_FIELDS.has(key)) | |
| .length === 1 | |
| ) { | |
| result.push(...config.ignores); | |
| } | |
| } | |
| // store result | |
| cache.ignores = result; | |
| dataCache.set(this, cache); | |
| return result; | |
| } | |
| /** | |
| * Indicates if the config array has been normalized. | |
| * @returns {boolean} True if the config array is normalized, false if not. | |
| */ | |
| isNormalized() { | |
| return this[ConfigArraySymbol.isNormalized]; | |
| } | |
| /** | |
| * Normalizes a config array by flattening embedded arrays and executing | |
| * config functions. | |
| * @param {Object} [context] The context object for config functions. | |
| * @returns {Promise<ConfigArray>} The current ConfigArray instance. | |
| */ | |
| async normalize(context = {}) { | |
| if (!this.isNormalized()) { | |
| const normalizedConfigs = await normalize( | |
| this, | |
| context, | |
| this.extraConfigTypes, | |
| ); | |
| this.length = 0; | |
| this.push( | |
| ...normalizedConfigs.map( | |
| this[ConfigArraySymbol.preprocessConfig].bind(this), | |
| ), | |
| ); | |
| this.forEach(assertValidBaseConfig); | |
| this[ConfigArraySymbol.isNormalized] = true; | |
| // prevent further changes | |
| Object.freeze(this); | |
| } | |
| return this; | |
| } | |
| /** | |
| * Normalizes a config array by flattening embedded arrays and executing | |
| * config functions. | |
| * @param {Object} [context] The context object for config functions. | |
| * @returns {ConfigArray} The current ConfigArray instance. | |
| */ | |
| normalizeSync(context = {}) { | |
| if (!this.isNormalized()) { | |
| const normalizedConfigs = normalizeSync( | |
| this, | |
| context, | |
| this.extraConfigTypes, | |
| ); | |
| this.length = 0; | |
| this.push( | |
| ...normalizedConfigs.map( | |
| this[ConfigArraySymbol.preprocessConfig].bind(this), | |
| ), | |
| ); | |
| this.forEach(assertValidBaseConfig); | |
| this[ConfigArraySymbol.isNormalized] = true; | |
| // prevent further changes | |
| Object.freeze(this); | |
| } | |
| return this; | |
| } | |
| /* eslint-disable class-methods-use-this -- Desired as instance methods */ | |
| /** | |
| * Finalizes the state of a config before being cached and returned by | |
| * `getConfig()`. Does nothing by default but is provided to be | |
| * overridden by subclasses as necessary. | |
| * @param {Object} config The config to finalize. | |
| * @returns {Object} The finalized config. | |
| */ | |
| [ConfigArraySymbol.finalizeConfig](config) { | |
| return config; | |
| } | |
| /** | |
| * Preprocesses a config during the normalization process. This is the | |
| * method to override if you want to convert an array item before it is | |
| * validated for the first time. For example, if you want to replace a | |
| * string with an object, this is the method to override. | |
| * @param {Object} config The config to preprocess. | |
| * @returns {Object} The config to use in place of the argument. | |
| */ | |
| [ConfigArraySymbol.preprocessConfig](config) { | |
| return config; | |
| } | |
| /* eslint-enable class-methods-use-this -- Desired as instance methods */ | |
| /** | |
| * Returns the config object for a given file path and a status that can be used to determine why a file has no config. | |
| * @param {string} filePath The path of a file to get a config for. | |
| * @returns {{ config?: Object, status: "ignored"|"external"|"unconfigured"|"matched" }} | |
| * An object with an optional property `config` and property `status`. | |
| * `config` is the config object for the specified file as returned by {@linkcode ConfigArray.getConfig}, | |
| * `status` a is one of the constants returned by {@linkcode ConfigArray.getConfigStatus}. | |
| */ | |
| getConfigWithStatus(filePath) { | |
| assertNormalized(this); | |
| const cache = this[ConfigArraySymbol.configCache]; | |
| // first check the cache for a filename match to avoid duplicate work | |
| if (cache.has(filePath)) { | |
| return cache.get(filePath); | |
| } | |
| // check to see if the file is outside the base path | |
| const relativeFilePath = toRelativePath( | |
| filePath, | |
| this.#namespacedBasePath, | |
| this.#path, | |
| ); | |
| if (EXTERNAL_PATH_REGEX.test(relativeFilePath)) { | |
| debug(`No config for file ${filePath} outside of base path`); | |
| // cache and return result | |
| cache.set(filePath, CONFIG_WITH_STATUS_EXTERNAL); | |
| return CONFIG_WITH_STATUS_EXTERNAL; | |
| } | |
| // next check to see if the file should be ignored | |
| // check if this should be ignored due to its directory | |
| if (this.isDirectoryIgnored(this.#path.dirname(filePath))) { | |
| debug(`Ignoring ${filePath} based on directory pattern`); | |
| // cache and return result | |
| cache.set(filePath, CONFIG_WITH_STATUS_IGNORED); | |
| return CONFIG_WITH_STATUS_IGNORED; | |
| } | |
| if (shouldIgnorePath(this.ignores, filePath, relativeFilePath)) { | |
| debug(`Ignoring ${filePath} based on file pattern`); | |
| // cache and return result | |
| cache.set(filePath, CONFIG_WITH_STATUS_IGNORED); | |
| return CONFIG_WITH_STATUS_IGNORED; | |
| } | |
| // filePath isn't automatically ignored, so try to construct config | |
| const matchingConfigIndices = []; | |
| let matchFound = false; | |
| const universalPattern = /^\*$|\/\*{1,2}$/u; | |
| this.forEach((config, index) => { | |
| if (!config.files) { | |
| if (!config.ignores) { | |
| debug(`Universal config found for ${filePath}`); | |
| matchingConfigIndices.push(index); | |
| return; | |
| } | |
| if (pathMatchesIgnores(filePath, relativeFilePath, config)) { | |
| debug( | |
| `Matching config found for ${filePath} (based on ignores: ${config.ignores})`, | |
| ); | |
| matchingConfigIndices.push(index); | |
| return; | |
| } | |
| debug( | |
| `Skipped config found for ${filePath} (based on ignores: ${config.ignores})`, | |
| ); | |
| return; | |
| } | |
| /* | |
| * If a config has a files pattern * or patterns ending in /** or /*, | |
| * and the filePath only matches those patterns, then the config is only | |
| * applied if there is another config where the filePath matches | |
| * a file with a specific extensions such as *.js. | |
| */ | |
| const universalFiles = config.files.filter(pattern => | |
| universalPattern.test(pattern), | |
| ); | |
| // universal patterns were found so we need to check the config twice | |
| if (universalFiles.length) { | |
| debug("Universal files patterns found. Checking carefully."); | |
| const nonUniversalFiles = config.files.filter( | |
| pattern => !universalPattern.test(pattern), | |
| ); | |
| // check that the config matches without the non-universal files first | |
| if ( | |
| nonUniversalFiles.length && | |
| pathMatches(filePath, relativeFilePath, { | |
| files: nonUniversalFiles, | |
| ignores: config.ignores, | |
| }) | |
| ) { | |
| debug(`Matching config found for ${filePath}`); | |
| matchingConfigIndices.push(index); | |
| matchFound = true; | |
| return; | |
| } | |
| // if there wasn't a match then check if it matches with universal files | |
| if ( | |
| universalFiles.length && | |
| pathMatches(filePath, relativeFilePath, { | |
| files: universalFiles, | |
| ignores: config.ignores, | |
| }) | |
| ) { | |
| debug(`Matching config found for ${filePath}`); | |
| matchingConfigIndices.push(index); | |
| return; | |
| } | |
| // if we make here, then there was no match | |
| return; | |
| } | |
| // the normal case | |
| if (pathMatches(filePath, relativeFilePath, config)) { | |
| debug(`Matching config found for ${filePath}`); | |
| matchingConfigIndices.push(index); | |
| matchFound = true; | |
| } | |
| }); | |
| // if matching both files and ignores, there will be no config to create | |
| if (!matchFound) { | |
| debug(`No matching configs found for ${filePath}`); | |
| // cache and return result | |
| cache.set(filePath, CONFIG_WITH_STATUS_UNCONFIGURED); | |
| return CONFIG_WITH_STATUS_UNCONFIGURED; | |
| } | |
| // check to see if there is a config cached by indices | |
| const indicesKey = matchingConfigIndices.toString(); | |
| let configWithStatus = cache.get(indicesKey); | |
| if (configWithStatus) { | |
| // also store for filename for faster lookup next time | |
| cache.set(filePath, configWithStatus); | |
| return configWithStatus; | |
| } | |
| // otherwise construct the config | |
| // eslint-disable-next-line array-callback-return, consistent-return -- rethrowConfigError always throws an error | |
| let finalConfig = matchingConfigIndices.reduce((result, index) => { | |
| try { | |
| return this[ConfigArraySymbol.schema].merge( | |
| result, | |
| this[index], | |
| ); | |
| } catch (validationError) { | |
| rethrowConfigError(this[index], index, validationError); | |
| } | |
| }, {}); | |
| finalConfig = this[ConfigArraySymbol.finalizeConfig](finalConfig); | |
| configWithStatus = Object.freeze({ | |
| config: finalConfig, | |
| status: "matched", | |
| }); | |
| cache.set(filePath, configWithStatus); | |
| cache.set(indicesKey, configWithStatus); | |
| return configWithStatus; | |
| } | |
| /** | |
| * Returns the config object for a given file path. | |
| * @param {string} filePath The path of a file to get a config for. | |
| * @returns {Object|undefined} The config object for this file or `undefined`. | |
| */ | |
| getConfig(filePath) { | |
| return this.getConfigWithStatus(filePath).config; | |
| } | |
| /** | |
| * Determines whether a file has a config or why it doesn't. | |
| * @param {string} filePath The path of the file to check. | |
| * @returns {"ignored"|"external"|"unconfigured"|"matched"} One of the following values: | |
| * * `"ignored"`: the file is ignored | |
| * * `"external"`: the file is outside the base path | |
| * * `"unconfigured"`: the file is not matched by any config | |
| * * `"matched"`: the file has a matching config | |
| */ | |
| getConfigStatus(filePath) { | |
| return this.getConfigWithStatus(filePath).status; | |
| } | |
| /** | |
| * Determines if the given filepath is ignored based on the configs. | |
| * @param {string} filePath The path of a file to check. | |
| * @returns {boolean} True if the path is ignored, false if not. | |
| * @deprecated Use `isFileIgnored` instead. | |
| */ | |
| isIgnored(filePath) { | |
| return this.isFileIgnored(filePath); | |
| } | |
| /** | |
| * Determines if the given filepath is ignored based on the configs. | |
| * @param {string} filePath The path of a file to check. | |
| * @returns {boolean} True if the path is ignored, false if not. | |
| */ | |
| isFileIgnored(filePath) { | |
| return this.getConfigStatus(filePath) === "ignored"; | |
| } | |
| /** | |
| * Determines if the given directory is ignored based on the configs. | |
| * This checks only default `ignores` that don't have `files` in the | |
| * same config. A pattern such as `/foo` be considered to ignore the directory | |
| * while a pattern such as `/foo/**` is not considered to ignore the | |
| * directory because it is matching files. | |
| * @param {string} directoryPath The path of a directory to check. | |
| * @returns {boolean} True if the directory is ignored, false if not. Will | |
| * return true for any directory that is not inside of `basePath`. | |
| * @throws {Error} When the `ConfigArray` is not normalized. | |
| */ | |
| isDirectoryIgnored(directoryPath) { | |
| assertNormalized(this); | |
| const relativeDirectoryPath = toRelativePath( | |
| directoryPath, | |
| this.#namespacedBasePath, | |
| this.#path, | |
| ); | |
| // basePath directory can never be ignored | |
| if (relativeDirectoryPath === "") { | |
| return false; | |
| } | |
| if (EXTERNAL_PATH_REGEX.test(relativeDirectoryPath)) { | |
| return true; | |
| } | |
| // first check the cache | |
| const cache = dataCache.get(this).directoryMatches; | |
| if (cache.has(relativeDirectoryPath)) { | |
| return cache.get(relativeDirectoryPath); | |
| } | |
| const directoryParts = relativeDirectoryPath.split("/"); | |
| let relativeDirectoryToCheck = ""; | |
| let result; | |
| /* | |
| * In order to get the correct gitignore-style ignores, where an | |
| * ignored parent directory cannot have any descendants unignored, | |
| * we need to check every directory starting at the parent all | |
| * the way down to the actual requested directory. | |
| * | |
| * We aggressively cache all of this info to make sure we don't | |
| * have to recalculate everything for every call. | |
| */ | |
| do { | |
| relativeDirectoryToCheck += `${directoryParts.shift()}/`; | |
| result = shouldIgnorePath( | |
| this.ignores, | |
| this.#path.join(this.basePath, relativeDirectoryToCheck), | |
| relativeDirectoryToCheck, | |
| ); | |
| cache.set(relativeDirectoryToCheck, result); | |
| } while (!result && directoryParts.length); | |
| // also cache the result for the requested path | |
| cache.set(relativeDirectoryPath, result); | |
| return result; | |
| } | |
| } | |
| Object.defineProperty(exports, "ObjectSchema", { | |
| enumerable: true, | |
| get: function () { return objectSchema.ObjectSchema; } | |
| }); | |
| exports.ConfigArray = ConfigArray; | |
| exports.ConfigArraySymbol = ConfigArraySymbol; | |