Assignment5_new/node_modules/eslint/lib/config/config-loader.js
2025-01-07 10:44:59 +05:30

720 lines
27 KiB
JavaScript

/**
* @fileoverview Utility to load config files
* @author Nicholas C. Zakas
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const path = require("node:path");
const fs = require("node:fs/promises");
const findUp = require("find-up");
const { pathToFileURL } = require("node:url");
const debug = require("debug")("eslint:config-loader");
const { FlatConfigArray } = require("./flat-config-array");
//-----------------------------------------------------------------------------
// Types
//-----------------------------------------------------------------------------
/**
* @typedef {import("../shared/types").FlatConfigObject} FlatConfigObject
* @typedef {import("../shared/types").FlatConfigArray} FlatConfigArray
* @typedef {Object} ConfigLoaderOptions
* @property {string|false|undefined} configFile The path to the config file to use.
* @property {string} cwd The current working directory.
* @property {boolean} ignoreEnabled Indicates if ignore patterns should be honored.
* @property {FlatConfigArray} [baseConfig] The base config to use.
* @property {Array<FlatConfigObject>} [defaultConfigs] The default configs to use.
* @property {Array<string>} [ignorePatterns] The ignore patterns to use.
* @property {FlatConfigObject|Array<FlatConfigObject>} overrideConfig The override config to use.
* @property {boolean} allowTS Indicates if TypeScript configuration files are allowed.
*/
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
const FLAT_CONFIG_FILENAMES = [
"eslint.config.js",
"eslint.config.mjs",
"eslint.config.cjs"
];
const TS_FLAT_CONFIG_FILENAMES = [
"eslint.config.ts",
"eslint.config.mts",
"eslint.config.cts"
];
const importedConfigFileModificationTime = new Map();
/**
* Asserts that the given file path is valid.
* @param {string} filePath The file path to check.
* @returns {void}
* @throws {Error} If `filePath` is not a non-empty string.
*/
function assertValidFilePath(filePath) {
if (!filePath || typeof filePath !== "string") {
throw new Error("'filePath' must be a non-empty string");
}
}
/**
* Asserts that a configuration exists. A configuration exists if any
* of the following are true:
* - `configFilePath` is defined.
* - `useConfigFile` is `false`.
* @param {string|undefined} configFilePath The path to the config file.
* @param {ConfigLoaderOptions} loaderOptions The options to use when loading configuration files.
* @returns {void}
* @throws {Error} If no configuration exists.
*/
function assertConfigurationExists(configFilePath, loaderOptions) {
const {
configFile: useConfigFile
} = loaderOptions;
if (!configFilePath && useConfigFile !== false) {
const error = new Error("Could not find config file.");
error.messageTemplate = "config-file-missing";
throw error;
}
}
/**
* Check if the file is a TypeScript file.
* @param {string} filePath The file path to check.
* @returns {boolean} `true` if the file is a TypeScript file, `false` if it's not.
*/
function isFileTS(filePath) {
const fileExtension = path.extname(filePath);
return /^\.[mc]?ts$/u.test(fileExtension);
}
/**
* Check if ESLint is running in Bun.
* @returns {boolean} `true` if the ESLint is running Bun, `false` if it's not.
*/
function isRunningInBun() {
return !!globalThis.Bun;
}
/**
* Check if ESLint is running in Deno.
* @returns {boolean} `true` if the ESLint is running in Deno, `false` if it's not.
*/
function isRunningInDeno() {
return !!globalThis.Deno;
}
/**
* Load the config array from the given filename.
* @param {string} filePath The filename to load from.
* @param {boolean} allowTS Indicates if TypeScript configuration files are allowed.
* @returns {Promise<any>} The config loaded from the config file.
*/
async function loadConfigFile(filePath, allowTS) {
debug(`Loading config from ${filePath}`);
const fileURL = pathToFileURL(filePath);
debug(`Config file URL is ${fileURL}`);
const mtime = (await fs.stat(filePath)).mtime.getTime();
/*
* Append a query with the config file's modification time (`mtime`) in order
* to import the current version of the config file. Without the query, `import()` would
* cache the config file module by the pathname only, and then always return
* the same version (the one that was actual when the module was imported for the first time).
*
* This ensures that the config file module is loaded and executed again
* if it has been changed since the last time it was imported.
* If it hasn't been changed, `import()` will just return the cached version.
*
* Note that we should not overuse queries (e.g., by appending the current time
* to always reload the config file module) as that could cause memory leaks
* because entries are never removed from the import cache.
*/
fileURL.searchParams.append("mtime", mtime);
/*
* With queries, we can bypass the import cache. However, when import-ing a CJS module,
* Node.js uses the require infrastructure under the hood. That includes the require cache,
* which caches the config file module by its file path (queries have no effect).
* Therefore, we also need to clear the require cache before importing the config file module.
* In order to get the same behavior with ESM and CJS config files, in particular - to reload
* the config file only if it has been changed, we track file modification times and clear
* the require cache only if the file has been changed.
*/
if (importedConfigFileModificationTime.get(filePath) !== mtime) {
delete require.cache[filePath];
}
const isTS = isFileTS(filePath);
const isBun = isRunningInBun();
const isDeno = isRunningInDeno();
/*
* If we are dealing with a TypeScript file, then we need to use `jiti` to load it
* in Node.js. Deno and Bun both allow native importing of TypeScript files.
*
* When Node.js supports native TypeScript imports, we can remove this check.
*/
if (allowTS && isTS && !isDeno && !isBun) {
// eslint-disable-next-line no-use-before-define -- `ConfigLoader.loadJiti` can be overwritten for testing
const { createJiti } = await ConfigLoader.loadJiti().catch(() => {
throw new Error("The 'jiti' library is required for loading TypeScript configuration files. Make sure to install it.");
});
// `createJiti` was added in jiti v2.
if (typeof createJiti !== "function") {
throw new Error("You are using an outdated version of the 'jiti' library. Please update to the latest version of 'jiti' to ensure compatibility and access to the latest features.");
}
/*
* Disabling `moduleCache` allows us to reload a
* config file when the last modified timestamp changes.
*/
const jiti = createJiti(__filename, { moduleCache: false, interopDefault: false });
const config = await jiti.import(fileURL.href);
importedConfigFileModificationTime.set(filePath, mtime);
return config?.default ?? config;
}
// fallback to normal runtime behavior
const config = (await import(fileURL)).default;
importedConfigFileModificationTime.set(filePath, mtime);
return config;
}
//-----------------------------------------------------------------------------
// Exports
//-----------------------------------------------------------------------------
/**
* Encapsulates the loading and caching of configuration files when looking up
* from the file being linted.
*/
class ConfigLoader {
/**
* Map of config file paths to the config arrays for those directories.
* @type {Map<string, FlatConfigArray|Promise<FlatConfigArray>>}
*/
#configArrays = new Map();
/**
* Map of absolute directory names to the config file paths for those directories.
* @type {Map<string, {configFilePath:string,basePath:string}|Promise<{configFilePath:string,basePath:string}>>}
*/
#configFilePaths = new Map();
/**
* The options to use when loading configuration files.
* @type {ConfigLoaderOptions}
*/
#options;
/**
* Creates a new instance.
* @param {ConfigLoaderOptions} options The options to use when loading configuration files.
*/
constructor(options) {
this.#options = options;
}
/**
* Determines which config file to use. This is determined by seeing if an
* override config file was specified, and if so, using it; otherwise, as long
* as override config file is not explicitly set to `false`, it will search
* upwards from `fromDirectory` for a file named `eslint.config.js`.
* @param {string} fromDirectory The directory from which to start searching.
* @returns {Promise<{configFilePath:string|undefined,basePath:string}>} Location information for
* the config file.
*/
async #locateConfigFileToUse(fromDirectory) {
// check cache first
if (this.#configFilePaths.has(fromDirectory)) {
return this.#configFilePaths.get(fromDirectory);
}
const resultPromise = ConfigLoader.locateConfigFileToUse({
useConfigFile: this.#options.configFile,
cwd: this.#options.cwd,
fromDirectory,
allowTS: this.#options.allowTS
});
// ensure `ConfigLoader.locateConfigFileToUse` is called only once for `fromDirectory`
this.#configFilePaths.set(fromDirectory, resultPromise);
// Unwrap the promise. This is primarily for the sync `getCachedConfigArrayForPath` method.
const result = await resultPromise;
this.#configFilePaths.set(fromDirectory, result);
return result;
}
/**
* Calculates the config array for this run based on inputs.
* @param {string} configFilePath The absolute path to the config file to use if not overridden.
* @param {string} basePath The base path to use for relative paths in the config file.
* @returns {Promise<FlatConfigArray>} The config array for `eslint`.
*/
async #calculateConfigArray(configFilePath, basePath) {
// check for cached version first
if (this.#configArrays.has(configFilePath)) {
return this.#configArrays.get(configFilePath);
}
const configsPromise = ConfigLoader.calculateConfigArray(configFilePath, basePath, this.#options);
// ensure `ConfigLoader.calculateConfigArray` is called only once for `configFilePath`
this.#configArrays.set(configFilePath, configsPromise);
// Unwrap the promise. This is primarily for the sync `getCachedConfigArrayForPath` method.
const configs = await configsPromise;
this.#configArrays.set(configFilePath, configs);
return configs;
}
/**
* Returns the config file path for the given directory or file. This will either use
* the override config file that was specified in the constructor options or
* search for a config file from the directory.
* @param {string} fileOrDirPath The file or directory path to get the config file path for.
* @returns {Promise<string|undefined>} The config file path or `undefined` if not found.
* @throws {Error} If `fileOrDirPath` is not a non-empty string.
* @throws {Error} If `fileOrDirPath` is not an absolute path.
*/
async findConfigFileForPath(fileOrDirPath) {
assertValidFilePath(fileOrDirPath);
const absoluteDirPath = path.resolve(this.#options.cwd, path.dirname(fileOrDirPath));
const { configFilePath } = await this.#locateConfigFileToUse(absoluteDirPath);
return configFilePath;
}
/**
* Returns a configuration object for the given file based on the CLI options.
* This is the same logic used by the ESLint CLI executable to determine
* configuration for each file it processes.
* @param {string} filePath The path of the file or directory to retrieve config for.
* @returns {Promise<ConfigData|undefined>} A configuration object for the file
* or `undefined` if there is no configuration data for the file.
* @throws {Error} If no configuration for `filePath` exists.
*/
async loadConfigArrayForFile(filePath) {
assertValidFilePath(filePath);
debug(`Calculating config for file ${filePath}`);
const configFilePath = await this.findConfigFileForPath(filePath);
assertConfigurationExists(configFilePath, this.#options);
return this.loadConfigArrayForDirectory(filePath);
}
/**
* Returns a configuration object for the given directory based on the CLI options.
* This is the same logic used by the ESLint CLI executable to determine
* configuration for each file it processes.
* @param {string} dirPath The path of the directory to retrieve config for.
* @returns {Promise<ConfigData|undefined>} A configuration object for the directory
* or `undefined` if there is no configuration data for the directory.
*/
async loadConfigArrayForDirectory(dirPath) {
assertValidFilePath(dirPath);
debug(`Calculating config for directory ${dirPath}`);
const absoluteDirPath = path.resolve(this.#options.cwd, path.dirname(dirPath));
const { configFilePath, basePath } = await this.#locateConfigFileToUse(absoluteDirPath);
debug(`Using config file ${configFilePath} and base path ${basePath}`);
return this.#calculateConfigArray(configFilePath, basePath);
}
/**
* Returns a configuration array for the given file based on the CLI options.
* This is a synchronous operation and does not read any files from disk. It's
* intended to be used in locations where we know the config file has already
* been loaded and we just need to get the configuration for a file.
* @param {string} filePath The path of the file to retrieve a config object for.
* @returns {ConfigData|undefined} A configuration object for the file
* or `undefined` if there is no configuration data for the file.
* @throws {Error} If `filePath` is not a non-empty string.
* @throws {Error} If `filePath` is not an absolute path.
* @throws {Error} If the config file was not already loaded.
*/
getCachedConfigArrayForFile(filePath) {
assertValidFilePath(filePath);
debug(`Looking up cached config for ${filePath}`);
return this.getCachedConfigArrayForPath(path.dirname(filePath));
}
/**
* Returns a configuration array for the given directory based on the CLI options.
* This is a synchronous operation and does not read any files from disk. It's
* intended to be used in locations where we know the config file has already
* been loaded and we just need to get the configuration for a file.
* @param {string} fileOrDirPath The path of the directory to retrieve a config object for.
* @returns {ConfigData|undefined} A configuration object for the directory
* or `undefined` if there is no configuration data for the directory.
* @throws {Error} If `dirPath` is not a non-empty string.
* @throws {Error} If `dirPath` is not an absolute path.
* @throws {Error} If the config file was not already loaded.
*/
getCachedConfigArrayForPath(fileOrDirPath) {
assertValidFilePath(fileOrDirPath);
debug(`Looking up cached config for ${fileOrDirPath}`);
const absoluteDirPath = path.resolve(this.#options.cwd, fileOrDirPath);
if (!this.#configFilePaths.has(absoluteDirPath)) {
throw new Error(`Could not find config file for ${fileOrDirPath}`);
}
const configFilePathInfo = this.#configFilePaths.get(absoluteDirPath);
if (typeof configFilePathInfo.then === "function") {
throw new Error(`Config file path for ${fileOrDirPath} has not yet been calculated or an error occurred during the calculation`);
}
const { configFilePath } = configFilePathInfo;
const configArray = this.#configArrays.get(configFilePath);
if (!configArray || typeof configArray.then === "function") {
throw new Error(`Config array for ${fileOrDirPath} has not yet been calculated or an error occurred during the calculation`);
}
return configArray;
}
/**
* Used to import the jiti dependency. This method is exposed internally for testing purposes.
* @returns {Promise<Record<string, unknown>>} A promise that fulfills with a module object
* or rejects with an error if jiti is not found.
*/
static loadJiti() {
return import("jiti");
}
/**
* Determines which config file to use. This is determined by seeing if an
* override config file was specified, and if so, using it; otherwise, as long
* as override config file is not explicitly set to `false`, it will search
* upwards from `fromDirectory` for a file named `eslint.config.js`.
* This method is exposed internally for testing purposes.
* @param {Object} [options] the options object
* @param {string|false|undefined} options.useConfigFile The path to the config file to use.
* @param {string} options.cwd Path to a directory that should be considered as the current working directory.
* @param {string} [options.fromDirectory] The directory from which to start searching. Defaults to `cwd`.
* @param {boolean} options.allowTS Indicates if TypeScript configuration files are allowed.
* @returns {Promise<{configFilePath:string|undefined,basePath:string}>} Location information for
* the config file.
*/
static async locateConfigFileToUse({ useConfigFile, cwd, fromDirectory = cwd, allowTS }) {
const configFilenames = allowTS
? [...FLAT_CONFIG_FILENAMES, ...TS_FLAT_CONFIG_FILENAMES]
: FLAT_CONFIG_FILENAMES;
// determine where to load config file from
let configFilePath;
let basePath = cwd;
if (typeof useConfigFile === "string") {
debug(`Override config file path is ${useConfigFile}`);
configFilePath = path.resolve(cwd, useConfigFile);
basePath = cwd;
} else if (useConfigFile !== false) {
debug("Searching for eslint.config.js");
configFilePath = await findUp(
configFilenames,
{ cwd: fromDirectory }
);
if (configFilePath) {
basePath = path.dirname(configFilePath);
}
}
return {
configFilePath,
basePath
};
}
/**
* Calculates the config array for this run based on inputs.
* This method is exposed internally for testing purposes.
* @param {string} configFilePath The absolute path to the config file to use if not overridden.
* @param {string} basePath The base path to use for relative paths in the config file.
* @param {ConfigLoaderOptions} options The options to use when loading configuration files.
* @returns {Promise<FlatConfigArray>} The config array for `eslint`.
*/
static async calculateConfigArray(configFilePath, basePath, options) {
const {
cwd,
baseConfig,
ignoreEnabled,
ignorePatterns,
overrideConfig,
defaultConfigs = [],
allowTS
} = options;
debug(`Calculating config array from config file ${configFilePath} and base path ${basePath}`);
const configs = new FlatConfigArray(baseConfig || [], { basePath, shouldIgnore: ignoreEnabled });
// load config file
if (configFilePath) {
debug(`Loading config file ${configFilePath}`);
const fileConfig = await loadConfigFile(configFilePath, allowTS);
if (Array.isArray(fileConfig)) {
configs.push(...fileConfig);
} else {
configs.push(fileConfig);
}
}
// add in any configured defaults
configs.push(...defaultConfigs);
// append command line ignore patterns
if (ignorePatterns && ignorePatterns.length > 0) {
let relativeIgnorePatterns;
/*
* If the config file basePath is different than the cwd, then
* the ignore patterns won't work correctly. Here, we adjust the
* ignore pattern to include the correct relative path. Patterns
* passed as `ignorePatterns` are relative to the cwd, whereas
* the config file basePath can be an ancestor of the cwd.
*/
if (basePath === cwd) {
relativeIgnorePatterns = ignorePatterns;
} else {
// relative path must only have Unix-style separators
const relativeIgnorePath = path.relative(basePath, cwd).replace(/\\/gu, "/");
relativeIgnorePatterns = ignorePatterns.map(pattern => {
const negated = pattern.startsWith("!");
const basePattern = negated ? pattern.slice(1) : pattern;
return (negated ? "!" : "") +
path.posix.join(relativeIgnorePath, basePattern);
});
}
/*
* Ignore patterns are added to the end of the config array
* so they can override default ignores.
*/
configs.push({
ignores: relativeIgnorePatterns
});
}
if (overrideConfig) {
if (Array.isArray(overrideConfig)) {
configs.push(...overrideConfig);
} else {
configs.push(overrideConfig);
}
}
await configs.normalize();
return configs;
}
}
/**
* Encapsulates the loading and caching of configuration files when looking up
* from the current working directory.
*/
class LegacyConfigLoader extends ConfigLoader {
/**
* The options to use when loading configuration files.
* @type {ConfigLoaderOptions}
*/
#options;
/**
* The cached config file path for this instance.
* @type {Promise<{configFilePath:string,basePath:string}|undefined>}
*/
#configFilePath;
/**
* The cached config array for this instance.
* @type {FlatConfigArray|Promise<FlatConfigArray>}
*/
#configArray;
/**
* Creates a new instance.
* @param {ConfigLoaderOptions} options The options to use when loading configuration files.
*/
constructor(options) {
super(options);
this.#options = options;
}
/**
* Determines which config file to use. This is determined by seeing if an
* override config file was specified, and if so, using it; otherwise, as long
* as override config file is not explicitly set to `false`, it will search
* upwards from the cwd for a file named `eslint.config.js`.
* @returns {Promise<{configFilePath:string|undefined,basePath:string}>} Location information for
* the config file.
*/
#locateConfigFileToUse() {
if (!this.#configFilePath) {
this.#configFilePath = ConfigLoader.locateConfigFileToUse({
useConfigFile: this.#options.configFile,
cwd: this.#options.cwd,
allowTS: this.#options.allowTS
});
}
return this.#configFilePath;
}
/**
* Calculates the config array for this run based on inputs.
* @param {string} configFilePath The absolute path to the config file to use if not overridden.
* @param {string} basePath The base path to use for relative paths in the config file.
* @returns {Promise<FlatConfigArray>} The config array for `eslint`.
*/
async #calculateConfigArray(configFilePath, basePath) {
// check for cached version first
if (this.#configArray) {
return this.#configArray;
}
// ensure `ConfigLoader.calculateConfigArray` is called only once
this.#configArray = ConfigLoader.calculateConfigArray(configFilePath, basePath, this.#options);
// Unwrap the promise. This is primarily for the sync `getCachedConfigArrayForPath` method.
this.#configArray = await this.#configArray;
return this.#configArray;
}
/**
* Returns the config file path for the given directory. This will either use
* the override config file that was specified in the constructor options or
* search for a config file from the directory of the file being linted.
* @param {string} dirPath The directory path to get the config file path for.
* @returns {Promise<string|undefined>} The config file path or `undefined` if not found.
* @throws {Error} If `fileOrDirPath` is not a non-empty string.
* @throws {Error} If `fileOrDirPath` is not an absolute path.
*/
async findConfigFileForPath(dirPath) {
assertValidFilePath(dirPath);
const { configFilePath } = await this.#locateConfigFileToUse();
return configFilePath;
}
/**
* Returns a configuration object for the given file based on the CLI options.
* This is the same logic used by the ESLint CLI executable to determine
* configuration for each file it processes.
* @param {string} dirPath The path of the directory to retrieve config for.
* @returns {Promise<ConfigData|undefined>} A configuration object for the file
* or `undefined` if there is no configuration data for the file.
*/
async loadConfigArrayForDirectory(dirPath) {
assertValidFilePath(dirPath);
debug(`[Legacy]: Calculating config for ${dirPath}`);
const { configFilePath, basePath } = await this.#locateConfigFileToUse();
debug(`[Legacy]: Using config file ${configFilePath} and base path ${basePath}`);
return this.#calculateConfigArray(configFilePath, basePath);
}
/**
* Returns a configuration array for the given directory based on the CLI options.
* This is a synchronous operation and does not read any files from disk. It's
* intended to be used in locations where we know the config file has already
* been loaded and we just need to get the configuration for a file.
* @param {string} dirPath The path of the directory to retrieve a config object for.
* @returns {ConfigData|undefined} A configuration object for the file
* or `undefined` if there is no configuration data for the file.
* @throws {Error} If `dirPath` is not a non-empty string.
* @throws {Error} If `dirPath` is not an absolute path.
* @throws {Error} If the config file was not already loaded.
*/
getCachedConfigArrayForPath(dirPath) {
assertValidFilePath(dirPath);
debug(`[Legacy]: Looking up cached config for ${dirPath}`);
if (!this.#configArray) {
throw new Error(`Could not find config file for ${dirPath}`);
}
if (typeof this.#configArray.then === "function") {
throw new Error(`Config array for ${dirPath} has not yet been calculated or an error occurred during the calculation`);
}
return this.#configArray;
}
}
module.exports = { ConfigLoader, LegacyConfigLoader };