Spaces:
Running
Running
// @ts-self-types="./index.d.ts" | |
import * as posixPath from './std__path/posix.js'; | |
import * as windowsPath from './std__path/windows.js'; | |
import minimatch from 'minimatch'; | |
import createDebug from 'debug'; | |
import { ObjectSchema } from '@eslint/object-schema'; | |
export { ObjectSchema } from '@eslint/object-schema'; | |
/** | |
* @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(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; | |
} | |
// 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; | |
} | |
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( | |
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; | |
} | |
} | |
export { ConfigArray, ConfigArraySymbol }; | |