@eslint/config-array
Advanced tools
Comparing version
@@ -8,2 +8,7 @@ /** | ||
/** | ||
* The base path for files and ignores. | ||
*/ | ||
basePath?: string; | ||
/** | ||
* The files to include. | ||
@@ -10,0 +15,0 @@ */ |
@@ -60,5 +60,5 @@ export { ObjectSchema } from "@eslint/object-schema"; | ||
* globbing operation to be faster. | ||
* @returns {string[]} An array of string patterns and functions to be ignored. | ||
* @returns {Object[]} An array of config objects representing global ignores. | ||
*/ | ||
get ignores(): string[]; | ||
get ignores(): any[]; | ||
/** | ||
@@ -65,0 +65,0 @@ * Indicates if the config array has been normalized. |
@@ -57,2 +57,3 @@ // @ts-self-types="./index.d.ts" | ||
}, | ||
basePath: NOOP_STRATEGY, | ||
files: NOOP_STRATEGY, | ||
@@ -129,2 +130,13 @@ ignores: NOOP_STRATEGY, | ||
const filesAndIgnoresSchema = Object.freeze({ | ||
basePath: { | ||
required: false, | ||
merge() { | ||
return undefined; | ||
}, | ||
validate(value) { | ||
if (typeof value !== "string") { | ||
throw new TypeError("Expected value to be a string."); | ||
} | ||
}, | ||
}, | ||
files: { | ||
@@ -226,3 +238,3 @@ required: false, | ||
*/ | ||
const META_FIELDS = new Set(["name"]); | ||
const META_FIELDS = new Set(["name", "basePath"]); | ||
@@ -353,2 +365,6 @@ /** | ||
if ("basePath" in config) { | ||
validateConfig.basePath = config.basePath; | ||
} | ||
if ("files" in config) { | ||
@@ -432,5 +448,7 @@ validateConfig.files = config.files; | ||
* @param {Object} config The config object to normalize patterns in. | ||
* @param {string} namespacedBasePath The namespaced base path of the directory to which config base path is relative. | ||
* @param {PathImpl} path Path-handling implementation. | ||
* @returns {Object} The normalized config object. | ||
*/ | ||
function normalizeConfigPatterns(config) { | ||
function normalizeConfigPatterns(config, namespacedBasePath, path) { | ||
if (!config) { | ||
@@ -440,5 +458,11 @@ return config; | ||
const hasBasePath = typeof config.basePath === "string"; | ||
let needsNormalization = false; | ||
if (Array.isArray(config.files)) { | ||
if (hasBasePath) { | ||
needsNormalization = true; | ||
} | ||
if (!needsNormalization && Array.isArray(config.files)) { | ||
needsNormalization = config.files.some(pattern => { | ||
@@ -462,2 +486,13 @@ if (Array.isArray(pattern)) { | ||
if (hasBasePath) { | ||
if (path.isAbsolute(config.basePath)) { | ||
newConfig.basePath = path.toNamespacedPath(config.basePath); | ||
} else { | ||
newConfig.basePath = path.resolve( | ||
namespacedBasePath, | ||
config.basePath, | ||
); | ||
} | ||
} | ||
if (Array.isArray(newConfig.files)) { | ||
@@ -486,6 +521,14 @@ newConfig.files = newConfig.files.map(pattern => { | ||
* @param {Array<string>} extraConfigTypes The config types to check. | ||
* @param {string} namespacedBasePath The namespaced base path of the directory to which config base paths are relative. | ||
* @param {PathImpl} path Path-handling implementation. | ||
* @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) { | ||
async function normalize( | ||
items, | ||
context, | ||
extraConfigTypes, | ||
namespacedBasePath, | ||
path, | ||
) { | ||
const allowFunctions = extraConfigTypes.includes("function"); | ||
@@ -530,3 +573,3 @@ const allowArrays = extraConfigTypes.includes("array"); | ||
for await (const config of asyncIterable) { | ||
configs.push(normalizeConfigPatterns(config)); | ||
configs.push(normalizeConfigPatterns(config, namespacedBasePath, path)); | ||
} | ||
@@ -544,6 +587,14 @@ | ||
* @param {Array<string>} extraConfigTypes The config types to check. | ||
* @param {string} namespacedBasePath The namespaced base path of the directory to which config base paths are relative. | ||
* @param {PathImpl} path Path-handling implementation | ||
* @returns {Array} A flattened array containing only config objects. | ||
* @throws {TypeError} When a config function returns a function. | ||
*/ | ||
function normalizeSync(items, context, extraConfigTypes) { | ||
function normalizeSync( | ||
items, | ||
context, | ||
extraConfigTypes, | ||
namespacedBasePath, | ||
path, | ||
) { | ||
const allowFunctions = extraConfigTypes.includes("function"); | ||
@@ -586,3 +637,3 @@ const allowArrays = extraConfigTypes.includes("array"); | ||
for (const config of flatTraverse(items)) { | ||
configs.push(normalizeConfigPatterns(config)); | ||
configs.push(normalizeConfigPatterns(config, namespacedBasePath, path)); | ||
} | ||
@@ -594,52 +645,83 @@ | ||
/** | ||
* 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, "/"); | ||
} | ||
/** | ||
* 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 {Array<{ basePath?: string, ignores: Array<string|((string) => boolean)>}>} configs Configuration objects containing `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} [basePathData] Additional data needed to recalculate paths for configuration objects | ||
* that have `basePath` property. | ||
* @param {string} [basePathData.basePath] Namespaced path to witch `relativeFilePath` is relative. | ||
* @param {PathImpl} [basePathData.path] Path-handling implementation. | ||
* @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); | ||
function shouldIgnorePath( | ||
configs, | ||
filePath, | ||
relativeFilePath, | ||
{ basePath, path } = {}, | ||
) { | ||
let shouldIgnore = false; | ||
for (const config of configs) { | ||
let relativeFilePathToCheck = relativeFilePath; | ||
if (config.basePath) { | ||
relativeFilePathToCheck = toRelativePath( | ||
path.resolve(basePath, relativeFilePath), | ||
config.basePath, | ||
path, | ||
); | ||
if ( | ||
relativeFilePathToCheck === "" || | ||
EXTERNAL_PATH_REGEX.test(relativeFilePathToCheck) | ||
) { | ||
continue; | ||
} | ||
// don't check negated patterns because we're not ignored yet | ||
if (!matcher.startsWith("!")) { | ||
return doMatch(relativeFilePath, matcher); | ||
if (relativeFilePath.endsWith("/")) { | ||
relativeFilePathToCheck += "/"; | ||
} | ||
// otherwise we're still not ignored | ||
return false; | ||
} | ||
shouldIgnore = config.ignores.reduce((ignored, matcher) => { | ||
if (!ignored) { | ||
if (typeof matcher === "function") { | ||
return matcher(filePath); | ||
} | ||
// only need to check negated patterns because we're ignored | ||
if (typeof matcher === "string" && matcher.startsWith("!")) { | ||
return !doMatch(relativeFilePath, matcher, { | ||
flipNegate: true, | ||
}); | ||
} | ||
// don't check negated patterns because we're not ignored yet | ||
if (!matcher.startsWith("!")) { | ||
return doMatch(relativeFilePathToCheck, matcher); | ||
} | ||
return ignored; | ||
}, false); | ||
} | ||
// otherwise we're still not ignored | ||
return 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) | ||
); | ||
// only need to check negated patterns because we're ignored | ||
if (typeof matcher === "string" && matcher.startsWith("!")) { | ||
return !doMatch(relativeFilePathToCheck, matcher, { | ||
flipNegate: true, | ||
}); | ||
} | ||
return ignored; | ||
}, shouldIgnore); | ||
} | ||
return shouldIgnore; | ||
} | ||
@@ -687,4 +769,8 @@ | ||
if (filePathMatchesPattern && config.ignores) { | ||
/* | ||
* Pass config object without `basePath`, because `relativeFilePath` is already | ||
* calculated as relative to it. | ||
*/ | ||
filePathMatchesPattern = !shouldIgnorePath( | ||
config.ignores, | ||
[{ ignores: config.ignores }], | ||
filePath, | ||
@@ -759,16 +845,2 @@ relativeFilePath, | ||
/** | ||
* 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, "/"); | ||
} | ||
//------------------------------------------------------------------------------ | ||
@@ -954,3 +1026,3 @@ // Public Interface | ||
* globbing operation to be faster. | ||
* @returns {string[]} An array of string patterns and functions to be ignored. | ||
* @returns {Object[]} An array of config objects representing global ignores. | ||
*/ | ||
@@ -982,3 +1054,3 @@ get ignores() { | ||
) { | ||
result.push(...config.ignores); | ||
result.push(config); | ||
} | ||
@@ -1014,2 +1086,4 @@ } | ||
this.extraConfigTypes, | ||
this.#namespacedBasePath, | ||
this.#path, | ||
); | ||
@@ -1044,2 +1118,4 @@ this.length = 0; | ||
this.extraConfigTypes, | ||
this.#namespacedBasePath, | ||
this.#path, | ||
); | ||
@@ -1109,3 +1185,3 @@ this.length = 0; | ||
const relativeFilePath = toRelativePath( | ||
const relativeToBaseFilePath = toRelativePath( | ||
filePath, | ||
@@ -1116,3 +1192,3 @@ this.#namespacedBasePath, | ||
if (EXTERNAL_PATH_REGEX.test(relativeFilePath)) { | ||
if (EXTERNAL_PATH_REGEX.test(relativeToBaseFilePath)) { | ||
debug(`No config for file ${filePath} outside of base path`); | ||
@@ -1136,3 +1212,8 @@ | ||
if (shouldIgnorePath(this.ignores, filePath, relativeFilePath)) { | ||
if ( | ||
shouldIgnorePath(this.ignores, filePath, relativeToBaseFilePath, { | ||
basePath: this.#namespacedBasePath, | ||
path: this.#path, | ||
}) | ||
) { | ||
debug(`Ignoring ${filePath} based on file pattern`); | ||
@@ -1152,2 +1233,17 @@ | ||
this.forEach((config, index) => { | ||
const relativeFilePath = config.basePath | ||
? toRelativePath( | ||
this.#path.resolve(this.#namespacedBasePath, filePath), | ||
config.basePath, | ||
this.#path, | ||
) | ||
: relativeToBaseFilePath; | ||
if (config.basePath && EXTERNAL_PATH_REGEX.test(relativeFilePath)) { | ||
debug( | ||
`Skipped config found for ${filePath} (based on config's base path: ${config.basePath}`, | ||
); | ||
return; | ||
} | ||
if (!config.files) { | ||
@@ -1160,13 +1256,33 @@ if (!config.ignores) { | ||
if (pathMatchesIgnores(filePath, relativeFilePath, config)) { | ||
if ( | ||
Object.keys(config).filter(key => !META_FIELDS.has(key)) | ||
.length === 1 | ||
) { | ||
debug( | ||
`Matching config found for ${filePath} (based on ignores: ${config.ignores})`, | ||
`Skipped config found for ${filePath} (global ignores)`, | ||
); | ||
matchingConfigIndices.push(index); | ||
return; | ||
} | ||
/* | ||
* Pass config object without `basePath`, because `relativeFilePath` is already | ||
* calculated as relative to it. | ||
*/ | ||
if ( | ||
shouldIgnorePath( | ||
[{ ignores: config.ignores }], | ||
filePath, | ||
relativeFilePath, | ||
) | ||
) { | ||
debug( | ||
`Skipped config found for ${filePath} (based on ignores: ${config.ignores})`, | ||
); | ||
return; | ||
} | ||
debug( | ||
`Skipped config found for ${filePath} (based on ignores: ${config.ignores})`, | ||
`Matching config found for ${filePath} (based on ignores: ${config.ignores})`, | ||
); | ||
matchingConfigIndices.push(index); | ||
return; | ||
@@ -1400,2 +1516,6 @@ } | ||
relativeDirectoryToCheck, | ||
{ | ||
basePath: this.#namespacedBasePath, | ||
path: this.#path, | ||
}, | ||
); | ||
@@ -1402,0 +1522,0 @@ |
@@ -7,2 +7,6 @@ /** | ||
/** | ||
* The base path for files and ignores. | ||
*/ | ||
basePath?: string; | ||
/** | ||
* The files to include. | ||
@@ -9,0 +13,0 @@ */ |
@@ -8,2 +8,7 @@ /** | ||
/** | ||
* The base path for files and ignores. | ||
*/ | ||
basePath?: string; | ||
/** | ||
* The files to include. | ||
@@ -10,0 +15,0 @@ */ |
{ | ||
"name": "@eslint/config-array", | ||
"version": "0.20.1", | ||
"version": "0.21.0", | ||
"description": "General purpose glob-based configuration matching.", | ||
@@ -58,7 +58,3 @@ "author": "Nicholas C. Zakas", | ||
"@types/minimatch": "^3.0.5", | ||
"c8": "^9.1.0", | ||
"mocha": "^10.4.0", | ||
"rollup": "^4.16.2", | ||
"rollup-plugin-copy": "^3.5.0", | ||
"typescript": "^5.4.5" | ||
"rollup-plugin-copy": "^3.5.0" | ||
}, | ||
@@ -65,0 +61,0 @@ "engines": { |
@@ -79,3 +79,3 @@ # Config Array | ||
This example reads in an object or array from `my.config.js` and passes it into the `ConfigArray` constructor as the first argument. The second argument is an object specifying the `basePath` (the directory in which `my.config.js` is found) and a `schema` to define the additional properties of a config object beyond `files`, `ignores`, and `name`. | ||
This example reads in an object or array from `my.config.js` and passes it into the `ConfigArray` constructor as the first argument. The second argument is an object specifying the `basePath` (the directory in which `my.config.js` is found) and a `schema` to define the additional properties of a config object beyond `files`, `ignores`, `basePath`, and `name`. | ||
@@ -169,2 +169,12 @@ ### Specifying a Schema | ||
}, | ||
// specific settings for files inside `src` directory | ||
{ | ||
name: "Source files", | ||
basePath: "src", | ||
files: ["**/*"], | ||
settings: { | ||
source: true, | ||
}, | ||
}, | ||
]; | ||
@@ -289,3 +299,3 @@ ``` | ||
- If a filename is not an absolute path, it will be resolved relative to the base path directory. | ||
- The returned config object never has `files`, `ignores`, or `name` properties; the only properties on the object will be the other configuration options specified. | ||
- The returned config object never has `files`, `ignores`, `basePath`, or `name` properties; the only properties on the object will be the other configuration options specified. | ||
- The config array caches configs, so subsequent calls to `getConfig()` with the same filename will return in a fast lookup rather than another calculation. | ||
@@ -292,0 +302,0 @@ - A config will only be generated if the filename matches an entry in a `files` key. A config will not be generated without matching a `files` key (configs without a `files` key are only applied when another config with a `files` key is applied; configs without `files` are never applied on their own). Any config with a `files` key entry that is `*` or ends with `/**` or `/*` will only be applied if another entry in the same `files` key matches or another config matches. |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
325670
1.88%3
-57.14%8674
2.75%370
2.78%