/** * @fileoverview Compatibility class for flat config. * @author Nicholas C. Zakas */ //----------------------------------------------------------------------------- // Requirements //----------------------------------------------------------------------------- import createDebug from 'debug'; import path from 'node:path'; import environments from '../conf/environments.js'; import { ConfigArrayFactory } from './config-array-factory.js'; //----------------------------------------------------------------------------- // Helpers //----------------------------------------------------------------------------- /** @typedef {import("../../shared/types").Environment} Environment */ /** @typedef {import("../../shared/types").Processor} Processor */ const debug = createDebug('eslintrc:flat-compat'); const cafactory = Symbol('cafactory'); /** * Translates an ESLintRC-style config object into a flag-config-style config * object. * @param {Object} eslintrcConfig An ESLintRC-style config object. * @param {Object} options Options to help translate the config. * @param {string} options.resolveConfigRelativeTo To the directory to resolve * configs from. * @param {string} options.resolvePluginsRelativeTo The directory to resolve * plugins from. * @param {ReadOnlyMap} options.pluginEnvironments A map of plugin environment * names to objects. * @param {ReadOnlyMap} options.pluginProcessors A map of plugin processor * names to objects. * @returns {Object} A flag-config-style config object. * @throws {Error} If a plugin or environment cannot be resolved. */ function translateESLintRC( eslintrcConfig, { resolveConfigRelativeTo, resolvePluginsRelativeTo, pluginEnvironments, pluginProcessors, } ) { const flatConfig = {}; const configs = []; const languageOptions = {}; const linterOptions = {}; const keysToCopy = ['settings', 'rules', 'processor']; const languageOptionsKeysToCopy = ['globals', 'parser', 'parserOptions']; const linterOptionsKeysToCopy = [ 'noInlineConfig', 'reportUnusedDisableDirectives', ]; // copy over simple translations for (const key of keysToCopy) { if (key in eslintrcConfig && typeof eslintrcConfig[key] !== 'undefined') { flatConfig[key] = eslintrcConfig[key]; } } // copy over languageOptions for (const key of languageOptionsKeysToCopy) { if (key in eslintrcConfig && typeof eslintrcConfig[key] !== 'undefined') { // create the languageOptions key in the flat config flatConfig.languageOptions = languageOptions; if (key === 'parser') { debug( `Resolving parser '${languageOptions[key]}' relative to ${resolveConfigRelativeTo}` ); if (eslintrcConfig[key].error) { throw eslintrcConfig[key].error; } languageOptions[key] = eslintrcConfig[key].definition; continue; } // clone any object values that are in the eslintrc config if (eslintrcConfig[key] && typeof eslintrcConfig[key] === 'object') { languageOptions[key] = { ...eslintrcConfig[key], }; } else { languageOptions[key] = eslintrcConfig[key]; } } } // copy over linterOptions for (const key of linterOptionsKeysToCopy) { if (key in eslintrcConfig && typeof eslintrcConfig[key] !== 'undefined') { flatConfig.linterOptions = linterOptions; linterOptions[key] = eslintrcConfig[key]; } } // move ecmaVersion a level up if (languageOptions.parserOptions) { if ('ecmaVersion' in languageOptions.parserOptions) { languageOptions.ecmaVersion = languageOptions.parserOptions.ecmaVersion; delete languageOptions.parserOptions.ecmaVersion; } if ('sourceType' in languageOptions.parserOptions) { languageOptions.sourceType = languageOptions.parserOptions.sourceType; delete languageOptions.parserOptions.sourceType; } // check to see if we even need parserOptions anymore and remove it if not if (Object.keys(languageOptions.parserOptions).length === 0) { delete languageOptions.parserOptions; } } // overrides if (eslintrcConfig.criteria) { flatConfig.files = [ (absoluteFilePath) => eslintrcConfig.criteria.test(absoluteFilePath), ]; } // translate plugins if (eslintrcConfig.plugins && typeof eslintrcConfig.plugins === 'object') { debug(`Translating plugins: ${eslintrcConfig.plugins}`); flatConfig.plugins = {}; for (const pluginName of Object.keys(eslintrcConfig.plugins)) { debug(`Translating plugin: ${pluginName}`); debug( `Resolving plugin '${pluginName} relative to ${resolvePluginsRelativeTo}` ); const { original: plugin, error } = eslintrcConfig.plugins[pluginName]; if (error) { throw error; } flatConfig.plugins[pluginName] = plugin; // create a config for any processors if (plugin.processors) { for (const processorName of Object.keys(plugin.processors)) { if (processorName.startsWith('.')) { debug(`Assigning processor: ${pluginName}/${processorName}`); configs.unshift({ files: [`**/*${processorName}`], processor: pluginProcessors.get(`${pluginName}/${processorName}`), }); } } } } } // translate env - must come after plugins if (eslintrcConfig.env && typeof eslintrcConfig.env === 'object') { for (const envName of Object.keys(eslintrcConfig.env)) { // only add environments that are true if (eslintrcConfig.env[envName]) { debug(`Translating environment: ${envName}`); if (environments.has(envName)) { // built-in environments should be defined first configs.unshift( ...translateESLintRC( { criteria: eslintrcConfig.criteria, ...environments.get(envName), }, { resolveConfigRelativeTo, resolvePluginsRelativeTo, } ) ); } else if (pluginEnvironments.has(envName)) { // if the environment comes from a plugin, it should come after the plugin config configs.push( ...translateESLintRC( { criteria: eslintrcConfig.criteria, ...pluginEnvironments.get(envName), }, { resolveConfigRelativeTo, resolvePluginsRelativeTo, } ) ); } } } } // only add if there are actually keys in the config if (Object.keys(flatConfig).length > 0) { configs.push(flatConfig); } return configs; } //----------------------------------------------------------------------------- // Exports //----------------------------------------------------------------------------- /** * A compatibility class for working with configs. */ class FlatCompat { constructor({ baseDirectory = process.cwd(), resolvePluginsRelativeTo = baseDirectory, recommendedConfig, allConfig, } = {}) { this.baseDirectory = baseDirectory; this.resolvePluginsRelativeTo = resolvePluginsRelativeTo; this[cafactory] = new ConfigArrayFactory({ cwd: baseDirectory, resolvePluginsRelativeTo, getEslintAllConfig() { if (!allConfig) { throw new TypeError( "Missing parameter 'allConfig' in FlatCompat constructor." ); } return allConfig; }, getEslintRecommendedConfig() { if (!recommendedConfig) { throw new TypeError( "Missing parameter 'recommendedConfig' in FlatCompat constructor." ); } return recommendedConfig; }, }); } /** * Translates an ESLintRC-style config into a flag-config-style config. * @param {Object} eslintrcConfig The ESLintRC-style config object. * @returns {Object} A flag-config-style config object. */ config(eslintrcConfig) { const eslintrcArray = this[cafactory].create(eslintrcConfig, { basePath: this.baseDirectory, }); const flatArray = []; let hasIgnorePatterns = false; eslintrcArray.forEach((configData) => { if (configData.type === 'config') { hasIgnorePatterns = hasIgnorePatterns || configData.ignorePattern; flatArray.push( ...translateESLintRC(configData, { resolveConfigRelativeTo: path.join( this.baseDirectory, '__placeholder.js' ), resolvePluginsRelativeTo: path.join( this.resolvePluginsRelativeTo, '__placeholder.js' ), pluginEnvironments: eslintrcArray.pluginEnvironments, pluginProcessors: eslintrcArray.pluginProcessors, }) ); } }); // combine ignorePatterns to emulate ESLintRC behavior better if (hasIgnorePatterns) { flatArray.unshift({ ignores: [ (filePath) => { // Compute the final config for this file. // This filters config array elements by `files`/`excludedFiles` then merges the elements. const finalConfig = eslintrcArray.extractConfig(filePath); // Test the `ignorePattern` properties of the final config. return ( Boolean(finalConfig.ignores) && finalConfig.ignores(filePath) ); }, ], }); } return flatArray; } /** * Translates the `env` section of an ESLintRC-style config. * @param {Object} envConfig The `env` section of an ESLintRC config. * @returns {Object[]} An array of flag-config objects representing the environments. */ env(envConfig) { return this.config({ env: envConfig, }); } /** * Translates the `extends` section of an ESLintRC-style config. * @param {...string} configsToExtend The names of the configs to load. * @returns {Object[]} An array of flag-config objects representing the config. */ extends(...configsToExtend) { return this.config({ extends: configsToExtend, }); } /** * Translates the `plugins` section of an ESLintRC-style config. * @param {...string} plugins The names of the plugins to load. * @returns {Object[]} An array of flag-config objects representing the plugins. */ plugins(...plugins) { return this.config({ plugins, }); } } export { FlatCompat };