| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604 | /** * @fileoverview Helper to locate and load configuration files. * @author Nicholas C. Zakas */"use strict";//------------------------------------------------------------------------------// Requirements//------------------------------------------------------------------------------const fs = require("fs"),    path = require("path"),    ConfigOps = require("./config-ops"),    validator = require("./config-validator"),    ModuleResolver = require("../util/module-resolver"),    naming = require("../util/naming"),    pathIsInside = require("path-is-inside"),    stripComments = require("strip-json-comments"),    stringify = require("json-stable-stringify-without-jsonify"),    requireUncached = require("require-uncached");const debug = require("debug")("eslint:config-file");//------------------------------------------------------------------------------// Helpers//------------------------------------------------------------------------------/** * Determines sort order for object keys for json-stable-stringify * * see: https://github.com/samn/json-stable-stringify#cmp * * @param   {Object} a The first comparison object ({key: akey, value: avalue}) * @param   {Object} b The second comparison object ({key: bkey, value: bvalue}) * @returns {number}   1 or -1, used in stringify cmp method */function sortByKey(a, b) {    return a.key > b.key ? 1 : -1;}//------------------------------------------------------------------------------// Private//------------------------------------------------------------------------------const CONFIG_FILES = [    ".eslintrc.js",    ".eslintrc.yaml",    ".eslintrc.yml",    ".eslintrc.json",    ".eslintrc",    "package.json"];const resolver = new ModuleResolver();/** * Convenience wrapper for synchronously reading file contents. * @param {string} filePath The filename to read. * @returns {string} The file contents, with the BOM removed. * @private */function readFile(filePath) {    return fs.readFileSync(filePath, "utf8").replace(/^\ufeff/, "");}/** * Determines if a given string represents a filepath or not using the same * conventions as require(), meaning that the first character must be nonalphanumeric * and not the @ sign which is used for scoped packages to be considered a file path. * @param {string} filePath The string to check. * @returns {boolean} True if it's a filepath, false if not. * @private */function isFilePath(filePath) {    return path.isAbsolute(filePath) || !/\w|@/.test(filePath.charAt(0));}/** * Loads a YAML configuration from a file. * @param {string} filePath The filename to load. * @returns {Object} The configuration object from the file. * @throws {Error} If the file cannot be read. * @private */function loadYAMLConfigFile(filePath) {    debug(`Loading YAML config file: ${filePath}`);    // lazy load YAML to improve performance when not used    const yaml = require("js-yaml");    try {        // empty YAML file can be null, so always use        return yaml.safeLoad(readFile(filePath)) || {};    } catch (e) {        debug(`Error reading YAML file: ${filePath}`);        e.message = `Cannot read config file: ${filePath}\nError: ${e.message}`;        throw e;    }}/** * Loads a JSON configuration from a file. * @param {string} filePath The filename to load. * @returns {Object} The configuration object from the file. * @throws {Error} If the file cannot be read. * @private */function loadJSONConfigFile(filePath) {    debug(`Loading JSON config file: ${filePath}`);    try {        return JSON.parse(stripComments(readFile(filePath)));    } catch (e) {        debug(`Error reading JSON file: ${filePath}`);        e.message = `Cannot read config file: ${filePath}\nError: ${e.message}`;        throw e;    }}/** * Loads a legacy (.eslintrc) configuration from a file. * @param {string} filePath The filename to load. * @returns {Object} The configuration object from the file. * @throws {Error} If the file cannot be read. * @private */function loadLegacyConfigFile(filePath) {    debug(`Loading config file: ${filePath}`);    // lazy load YAML to improve performance when not used    const yaml = require("js-yaml");    try {        return yaml.safeLoad(stripComments(readFile(filePath))) || /* istanbul ignore next */ {};    } catch (e) {        debug(`Error reading YAML file: ${filePath}`);        e.message = `Cannot read config file: ${filePath}\nError: ${e.message}`;        throw e;    }}/** * Loads a JavaScript configuration from a file. * @param {string} filePath The filename to load. * @returns {Object} The configuration object from the file. * @throws {Error} If the file cannot be read. * @private */function loadJSConfigFile(filePath) {    debug(`Loading JS config file: ${filePath}`);    try {        return requireUncached(filePath);    } catch (e) {        debug(`Error reading JavaScript file: ${filePath}`);        e.message = `Cannot read config file: ${filePath}\nError: ${e.message}`;        throw e;    }}/** * Loads a configuration from a package.json file. * @param {string} filePath The filename to load. * @returns {Object} The configuration object from the file. * @throws {Error} If the file cannot be read. * @private */function loadPackageJSONConfigFile(filePath) {    debug(`Loading package.json config file: ${filePath}`);    try {        return loadJSONConfigFile(filePath).eslintConfig || null;    } catch (e) {        debug(`Error reading package.json file: ${filePath}`);        e.message = `Cannot read config file: ${filePath}\nError: ${e.message}`;        throw e;    }}/** * Creates an error to notify about a missing config to extend from. * @param {string} configName The name of the missing config. * @returns {Error} The error object to throw * @private */function configMissingError(configName) {    const error = new Error(`Failed to load config "${configName}" to extend from.`);    error.messageTemplate = "extend-config-missing";    error.messageData = {        configName    };    return error;}/** * Loads a configuration file regardless of the source. Inspects the file path * to determine the correctly way to load the config file. * @param {Object} file The path to the configuration. * @returns {Object} The configuration information. * @private */function loadConfigFile(file) {    const filePath = file.filePath;    let config;    switch (path.extname(filePath)) {        case ".js":            config = loadJSConfigFile(filePath);            if (file.configName) {                config = config.configs[file.configName];                if (!config) {                    throw configMissingError(file.configFullName);                }            }            break;        case ".json":            if (path.basename(filePath) === "package.json") {                config = loadPackageJSONConfigFile(filePath);                if (config === null) {                    return null;                }            } else {                config = loadJSONConfigFile(filePath);            }            break;        case ".yaml":        case ".yml":            config = loadYAMLConfigFile(filePath);            break;        default:            config = loadLegacyConfigFile(filePath);    }    return ConfigOps.merge(ConfigOps.createEmptyConfig(), config);}/** * Writes a configuration file in JSON format. * @param {Object} config The configuration object to write. * @param {string} filePath The filename to write to. * @returns {void} * @private */function writeJSONConfigFile(config, filePath) {    debug(`Writing JSON config file: ${filePath}`);    const content = stringify(config, { cmp: sortByKey, space: 4 });    fs.writeFileSync(filePath, content, "utf8");}/** * Writes a configuration file in YAML format. * @param {Object} config The configuration object to write. * @param {string} filePath The filename to write to. * @returns {void} * @private */function writeYAMLConfigFile(config, filePath) {    debug(`Writing YAML config file: ${filePath}`);    // lazy load YAML to improve performance when not used    const yaml = require("js-yaml");    const content = yaml.safeDump(config, { sortKeys: true });    fs.writeFileSync(filePath, content, "utf8");}/** * Writes a configuration file in JavaScript format. * @param {Object} config The configuration object to write. * @param {string} filePath The filename to write to. * @returns {void} * @private */function writeJSConfigFile(config, filePath) {    debug(`Writing JS config file: ${filePath}`);    const content = `module.exports = ${stringify(config, { cmp: sortByKey, space: 4 })};`;    fs.writeFileSync(filePath, content, "utf8");}/** * Writes a configuration file. * @param {Object} config The configuration object to write. * @param {string} filePath The filename to write to. * @returns {void} * @throws {Error} When an unknown file type is specified. * @private */function write(config, filePath) {    switch (path.extname(filePath)) {        case ".js":            writeJSConfigFile(config, filePath);            break;        case ".json":            writeJSONConfigFile(config, filePath);            break;        case ".yaml":        case ".yml":            writeYAMLConfigFile(config, filePath);            break;        default:            throw new Error("Can't write to unknown file type.");    }}/** * Determines the base directory for node packages referenced in a config file. * This does not include node_modules in the path so it can be used for all * references relative to a config file. * @param {string} configFilePath The config file referencing the file. * @returns {string} The base directory for the file path. * @private */function getBaseDir(configFilePath) {    // calculates the path of the project including ESLint as dependency    const projectPath = path.resolve(__dirname, "../../../");    if (configFilePath && pathIsInside(configFilePath, projectPath)) {        // be careful of https://github.com/substack/node-resolve/issues/78        return path.join(path.resolve(configFilePath));    }    /*     * default to ESLint project path since it's unlikely that plugins will be     * in this directory     */    return path.join(projectPath);}/** * Determines the lookup path, including node_modules, for package * references relative to a config file. * @param {string} configFilePath The config file referencing the file. * @returns {string} The lookup path for the file path. * @private */function getLookupPath(configFilePath) {    const basedir = getBaseDir(configFilePath);    return path.join(basedir, "node_modules");}/** * Resolves a eslint core config path * @param {string} name The eslint config name. * @returns {string} The resolved path of the config. * @private */function getEslintCoreConfigPath(name) {    if (name === "eslint:recommended") {        /*         * Add an explicit substitution for eslint:recommended to         * conf/eslint-recommended.js.         */        return path.resolve(__dirname, "../../conf/eslint-recommended.js");    }    if (name === "eslint:all") {        /*         * Add an explicit substitution for eslint:all to conf/eslint-all.js         */        return path.resolve(__dirname, "../../conf/eslint-all.js");    }    throw configMissingError(name);}/** * Applies values from the "extends" field in a configuration file. * @param {Object} config The configuration information. * @param {Config} configContext Plugin context for the config instance * @param {string} filePath The file path from which the configuration information *      was loaded. * @param {string} [relativeTo] The path to resolve relative to. * @returns {Object} A new configuration object with all of the "extends" fields *      loaded and merged. * @private */function applyExtends(config, configContext, filePath, relativeTo) {    let configExtends = config.extends;    // normalize into an array for easier handling    if (!Array.isArray(config.extends)) {        configExtends = [config.extends];    }    // Make the last element in an array take the highest precedence    return configExtends.reduceRight((previousValue, parentPath) => {        try {            let extensionPath;            if (parentPath.startsWith("eslint:")) {                extensionPath = getEslintCoreConfigPath(parentPath);            } else if (isFilePath(parentPath)) {                /*                 * If the `extends` path is relative, use the directory of the current configuration                 * file as the reference point. Otherwise, use as-is.                 */                extensionPath = (path.isAbsolute(parentPath)                    ? parentPath                    : path.join(relativeTo || path.dirname(filePath), parentPath)                );            } else {                extensionPath = parentPath;            }            debug(`Loading ${extensionPath}`);            // eslint-disable-next-line no-use-before-define            return ConfigOps.merge(load(extensionPath, configContext, relativeTo), previousValue);        } catch (e) {            /*             * If the file referenced by `extends` failed to load, add the path             * to the configuration file that referenced it to the error             * message so the user is able to see where it was referenced from,             * then re-throw.             */            e.message += `\nReferenced from: ${filePath}`;            throw e;        }    }, config);}/** * Resolves a configuration file path into the fully-formed path, whether filename * or package name. * @param {string} filePath The filepath to resolve. * @param {string} [relativeTo] The path to resolve relative to. * @returns {Object} An object containing 3 properties: * - 'filePath' (required) the resolved path that can be used directly to load the configuration. * - 'configName' the name of the configuration inside the plugin. * - 'configFullName' (required) the name of the configuration as used in the eslint config(e.g. 'plugin:node/recommended'), *     or the absolute path to a config file. This should uniquely identify a config. * @private */function resolve(filePath, relativeTo) {    if (isFilePath(filePath)) {        const fullPath = path.resolve(relativeTo || "", filePath);        return { filePath: fullPath, configFullName: fullPath };    }    let normalizedPackageName;    if (filePath.startsWith("plugin:")) {        const configFullName = filePath;        const pluginName = filePath.slice(7, filePath.lastIndexOf("/"));        const configName = filePath.slice(filePath.lastIndexOf("/") + 1);        normalizedPackageName = naming.normalizePackageName(pluginName, "eslint-plugin");        debug(`Attempting to resolve ${normalizedPackageName}`);        return {            filePath: resolver.resolve(normalizedPackageName, getLookupPath(relativeTo)),            configName,            configFullName        };    }    normalizedPackageName = naming.normalizePackageName(filePath, "eslint-config");    debug(`Attempting to resolve ${normalizedPackageName}`);    return {        filePath: resolver.resolve(normalizedPackageName, getLookupPath(relativeTo)),        configFullName: filePath    };}/** * Loads a configuration file from the given file path. * @param {Object} resolvedPath The value from calling resolve() on a filename or package name. * @param {Config} configContext Plugins context * @returns {Object} The configuration information. */function loadFromDisk(resolvedPath, configContext) {    const dirname = path.dirname(resolvedPath.filePath),        lookupPath = getLookupPath(dirname);    let config = loadConfigFile(resolvedPath);    if (config) {        // ensure plugins are properly loaded first        if (config.plugins) {            configContext.plugins.loadAll(config.plugins);        }        // include full path of parser if present        if (config.parser) {            if (isFilePath(config.parser)) {                config.parser = path.resolve(dirname || "", config.parser);            } else {                config.parser = resolver.resolve(config.parser, lookupPath);            }        }        const ruleMap = configContext.linterContext.getRules();        // validate the configuration before continuing        validator.validate(config, resolvedPath.configFullName, ruleMap.get.bind(ruleMap), configContext.linterContext.environments);        /*         * If an `extends` property is defined, it represents a configuration file to use as         * a "parent". Load the referenced file and merge the configuration recursively.         */        if (config.extends) {            config = applyExtends(config, configContext, resolvedPath.filePath, dirname);        }    }    return config;}/** * Loads a config object, applying extends if present. * @param {Object} configObject a config object to load * @param {Config} configContext Context for the config instance * @returns {Object} the config object with extends applied if present, or the passed config if not * @private */function loadObject(configObject, configContext) {    return configObject.extends ? applyExtends(configObject, configContext, "") : configObject;}/** * Loads a config object from the config cache based on its filename, falling back to the disk if the file is not yet * cached. * @param {string} filePath the path to the config file * @param {Config} configContext Context for the config instance * @param {string} [relativeTo] The path to resolve relative to. * @returns {Object} the parsed config object (empty object if there was a parse error) * @private */function load(filePath, configContext, relativeTo) {    const resolvedPath = resolve(filePath, relativeTo);    const cachedConfig = configContext.configCache.getConfig(resolvedPath.configFullName);    if (cachedConfig) {        return cachedConfig;    }    const config = loadFromDisk(resolvedPath, configContext);    if (config) {        config.filePath = resolvedPath.filePath;        config.baseDirectory = path.dirname(resolvedPath.filePath);        configContext.configCache.setConfig(resolvedPath.configFullName, config);    }    return config;}//------------------------------------------------------------------------------// Public Interface//------------------------------------------------------------------------------module.exports = {    getBaseDir,    getLookupPath,    load,    loadObject,    resolve,    write,    applyExtends,    CONFIG_FILES,    /**     * Retrieves the configuration filename for a given directory. It loops over all     * of the valid configuration filenames in order to find the first one that exists.     * @param {string} directory The directory to check for a config file.     * @returns {?string} The filename of the configuration file for the directory     *      or null if there is no configuration file in the directory.     */    getFilenameForDirectory(directory) {        for (let i = 0, len = CONFIG_FILES.length; i < len; i++) {            const filename = path.join(directory, CONFIG_FILES[i]);            if (fs.existsSync(filename) && fs.statSync(filename).isFile()) {                return filename;            }        }        return null;    }};
 |