flat-compat.js 10.7 KB
/**
 * @fileoverview Compatibility class for flat config.
 * @author Nicholas C. Zakas
 */

//-----------------------------------------------------------------------------
// Requirements
//-----------------------------------------------------------------------------

import createDebug from "debug";
import path from "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<string,Environment>} options.pluginEnvironments A map of plugin environment
 *      names to objects.
 * @param {ReadOnlyMap<string,Processor>} options.pluginProcessors A map of plugin processor
 *      names to objects.
 * @returns {Object} A flag-config-style config object.
 */
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"];

    // check for special settings for eslint:all and eslint:recommended:
    if (eslintrcConfig.settings) {
        if (eslintrcConfig.settings["eslint:all"] === true) {
            return ["eslint:all"];
        }

        if (eslintrcConfig.settings["eslint:recommended"] === true) {
            return ["eslint:recommended"];
        }
    }

    // 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 { definition: 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(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(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
    } = {}) {
        this.baseDirectory = baseDirectory;
        this.resolvePluginsRelativeTo = resolvePluginsRelativeTo;
        this[cafactory] = new ConfigArrayFactory({
            cwd: baseDirectory,
            resolvePluginsRelativeTo,
            getEslintAllConfig: () => ({ settings: { "eslint:all": true } }),
            getEslintRecommendedConfig: () => ({ settings: { "eslint:recommended": true } })
        });
    }

    /**
     * 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} A flag-config object 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} A flag-config object 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} A flag-config object representing the plugins.
     */
    plugins(...plugins) {
        return this.config({
            plugins
        });
    }
}

export { FlatCompat };