123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703 |
- /**
- * @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("../config/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;
- }
- /**
- * 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.
- * @param {ConfigLoaderOptions} options The options to use when loading configuration files.
- * @returns {Promise<FlatConfigArray>} The config array for `eslint`.
- */
- async function 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;
- }
- //-----------------------------------------------------------------------------
- // 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>}
- */
- #configArrays = new Map();
- /**
- * Map of absolute directory names to the config file paths for those directories.
- * @type {Map<string, {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 configFilenames = this.#options.allowTS
- ? [...FLAT_CONFIG_FILENAMES, ...TS_FLAT_CONFIG_FILENAMES]
- : FLAT_CONFIG_FILENAMES;
- // determine where to load config file from
- let configFilePath;
- const {
- cwd,
- configFile: useConfigFile
- } = this.#options;
- 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);
- }
- }
- // cache the result
- this.#configFilePaths.set(fromDirectory, { configFilePath, basePath });
- return {
- configFilePath,
- basePath
- };
- }
- /**
- * 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 configs = await calculateConfigArray(configFilePath, basePath, this.#options);
- // cache the config array for this instance
- 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 { configFilePath } = this.#configFilePaths.get(absoluteDirPath);
- return this.#configArrays.get(configFilePath);
- }
- /**
- * 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");
- }
- }
- /**
- * 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 {{configFilePath:string,basePath:string}|undefined}
- */
- #configFilePath;
- /**
- * The cached config array for this instance.
- * @type {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.
- */
- async #locateConfigFileToUse() {
- // check cache first
- if (this.#configFilePath) {
- return this.#configFilePath;
- }
- const configFilenames = this.#options.allowTS
- ? [...FLAT_CONFIG_FILENAMES, ...TS_FLAT_CONFIG_FILENAMES]
- : FLAT_CONFIG_FILENAMES;
- // determine where to load config file from
- let configFilePath;
- const {
- cwd,
- configFile: useConfigFile
- } = this.#options;
- let basePath = cwd;
- if (typeof useConfigFile === "string") {
- debug(`[Legacy]: Override config file path is ${useConfigFile}`);
- configFilePath = path.resolve(cwd, useConfigFile);
- basePath = cwd;
- } else if (useConfigFile !== false) {
- debug("[Legacy]: Searching for eslint.config.js");
- configFilePath = await findUp(
- configFilenames,
- { cwd }
- );
- if (configFilePath) {
- basePath = path.dirname(configFilePath);
- }
- }
- // cache the result
- this.#configFilePath = { configFilePath, basePath };
- return {
- configFilePath,
- basePath
- };
- }
- /**
- * 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;
- }
- const configs = await calculateConfigArray(configFilePath, basePath, this.#options);
- // cache the config array for this instance
- this.#configArray = configs;
- return configs;
- }
- /**
- * 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}`);
- }
- return this.#configArray;
- }
- }
- module.exports = { ConfigLoader, LegacyConfigLoader };
|