mirror of https://github.com/lukechilds/node.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
428 lines
14 KiB
428 lines
14 KiB
/**
|
|
* @fileoverview Responsible for loading config files
|
|
* @author Seth McLaughlin
|
|
* @copyright 2014 Nicholas C. Zakas. All rights reserved.
|
|
* @copyright 2013 Seth McLaughlin. All rights reserved.
|
|
* @copyright 2014 Michael McLaughlin. All rights reserved.
|
|
*/
|
|
"use strict";
|
|
|
|
//------------------------------------------------------------------------------
|
|
// Requirements
|
|
//------------------------------------------------------------------------------
|
|
|
|
var fs = require("fs"),
|
|
path = require("path"),
|
|
environments = require("../conf/environments"),
|
|
util = require("./util"),
|
|
FileFinder = require("./file-finder"),
|
|
stripComments = require("strip-json-comments"),
|
|
assign = require("object-assign"),
|
|
debug = require("debug"),
|
|
yaml = require("js-yaml"),
|
|
userHome = require("user-home"),
|
|
isAbsolutePath = require("path-is-absolute"),
|
|
validator = require("./config-validator");
|
|
|
|
//------------------------------------------------------------------------------
|
|
// Constants
|
|
//------------------------------------------------------------------------------
|
|
|
|
var LOCAL_CONFIG_FILENAME = ".eslintrc",
|
|
PACKAGE_CONFIG_FILENAME = "package.json",
|
|
PACKAGE_CONFIG_FIELD_NAME = "eslintConfig",
|
|
PERSONAL_CONFIG_PATH = userHome ? path.join(userHome, LOCAL_CONFIG_FILENAME) : null;
|
|
|
|
//------------------------------------------------------------------------------
|
|
// Private
|
|
//------------------------------------------------------------------------------
|
|
|
|
var loadedPlugins = Object.create(null);
|
|
|
|
//------------------------------------------------------------------------------
|
|
// Helpers
|
|
//------------------------------------------------------------------------------
|
|
|
|
debug = debug("eslint:config");
|
|
|
|
/**
|
|
* 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 isAbsolutePath(filePath) || !/\w|@/.test(filePath[0]);
|
|
}
|
|
|
|
/**
|
|
* Load and parse a JSON config object from a file.
|
|
* @param {string} filePath the path to the JSON config file
|
|
* @returns {Object} the parsed config object (empty object if there was a parse error)
|
|
* @private
|
|
*/
|
|
function loadConfig(filePath) {
|
|
var config = {};
|
|
|
|
if (filePath) {
|
|
|
|
if (isFilePath(filePath)) {
|
|
try {
|
|
config = yaml.safeLoad(stripComments(fs.readFileSync(filePath, "utf8"))) || {};
|
|
} catch (e) {
|
|
debug("Error reading YAML file: " + filePath);
|
|
e.message = "Cannot read config file: " + filePath + "\nError: " + e.message;
|
|
throw e;
|
|
}
|
|
|
|
if (path.basename(filePath) === PACKAGE_CONFIG_FILENAME) {
|
|
config = config[PACKAGE_CONFIG_FIELD_NAME] || {};
|
|
}
|
|
|
|
} else {
|
|
|
|
// it's a package
|
|
if (filePath.indexOf("eslint-config-") === -1) {
|
|
if (filePath.indexOf("@") === 0) {
|
|
// for scoped packages, insert the eslint-config after the first /
|
|
filePath = filePath.replace(/^([^\/]+\/)(.*)$/, "$1eslint-config-$2");
|
|
} else {
|
|
filePath = "eslint-config-" + filePath;
|
|
}
|
|
}
|
|
|
|
config = util.mergeConfigs(config, require(filePath));
|
|
}
|
|
|
|
validator.validate(config, filePath);
|
|
|
|
// 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) {
|
|
var configExtends = config.extends;
|
|
|
|
if (!Array.isArray(config.extends)) {
|
|
configExtends = [config.extends];
|
|
}
|
|
|
|
// Make the last element in an array take the highest precedence
|
|
config = configExtends.reduceRight(function (previousValue, parentPath) {
|
|
|
|
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.
|
|
parentPath = (!isAbsolutePath(parentPath) ?
|
|
path.join(path.dirname(filePath), parentPath) :
|
|
parentPath
|
|
);
|
|
}
|
|
|
|
try {
|
|
return util.mergeConfigs(loadConfig(parentPath), 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);
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return config;
|
|
}
|
|
|
|
/**
|
|
* Load configuration for all plugins provided.
|
|
* @param {string[]} pluginNames An array of plugin names which should be loaded.
|
|
* @returns {Object} all plugin configurations merged together
|
|
*/
|
|
function getPluginsConfig(pluginNames) {
|
|
var pluginConfig = {};
|
|
|
|
pluginNames.forEach(function (pluginName) {
|
|
var pluginNamespace = util.getNamespace(pluginName),
|
|
pluginNameWithoutNamespace = util.removeNameSpace(pluginName),
|
|
pluginNameWithoutPrefix = util.removePluginPrefix(pluginNameWithoutNamespace),
|
|
plugin = {},
|
|
rules = {};
|
|
|
|
if (!loadedPlugins[pluginNameWithoutPrefix]) {
|
|
try {
|
|
plugin = require(pluginNamespace + util.PLUGIN_NAME_PREFIX + pluginNameWithoutPrefix);
|
|
loadedPlugins[pluginNameWithoutPrefix] = plugin;
|
|
} catch(err) {
|
|
debug("Failed to load plugin configuration for " + pluginNameWithoutPrefix + ". Proceeding without it.");
|
|
plugin = { rulesConfig: {}};
|
|
}
|
|
} else {
|
|
plugin = loadedPlugins[pluginNameWithoutPrefix];
|
|
}
|
|
|
|
if (!plugin.rulesConfig) {
|
|
plugin.rulesConfig = {};
|
|
}
|
|
|
|
Object.keys(plugin.rulesConfig).forEach(function(item) {
|
|
rules[pluginNameWithoutPrefix + "/" + item] = plugin.rulesConfig[item];
|
|
});
|
|
|
|
pluginConfig = util.mergeConfigs(pluginConfig, rules);
|
|
});
|
|
|
|
return {rules: pluginConfig};
|
|
}
|
|
|
|
/**
|
|
* Get personal config object from ~/.eslintrc.
|
|
* @returns {Object} the personal config object (empty object if there is no personal config)
|
|
* @private
|
|
*/
|
|
function getPersonalConfig() {
|
|
var config = {};
|
|
|
|
if (PERSONAL_CONFIG_PATH && fs.existsSync(PERSONAL_CONFIG_PATH)) {
|
|
debug("Using personal config");
|
|
config = loadConfig(PERSONAL_CONFIG_PATH);
|
|
}
|
|
|
|
return config;
|
|
}
|
|
|
|
/**
|
|
* Get a local config object.
|
|
* @param {Object} thisConfig A Config object.
|
|
* @param {string} directory The directory to start looking in for a local config file.
|
|
* @returns {Object} The local config object, or an empty object if there is no local config.
|
|
*/
|
|
function getLocalConfig(thisConfig, directory) {
|
|
var found,
|
|
i,
|
|
localConfig,
|
|
localConfigFile,
|
|
config = {},
|
|
localConfigFiles = thisConfig.findLocalConfigFiles(directory),
|
|
numFiles = localConfigFiles.length;
|
|
|
|
for (i = 0; i < numFiles; i++) {
|
|
|
|
localConfigFile = localConfigFiles[i];
|
|
|
|
// Don't consider the personal config file in the home directory.
|
|
if (localConfigFile === PERSONAL_CONFIG_PATH) {
|
|
continue;
|
|
}
|
|
|
|
debug("Loading " + localConfigFile);
|
|
localConfig = loadConfig(localConfigFile);
|
|
|
|
// Don't consider a local config file found if the config is empty.
|
|
if (!Object.keys(localConfig).length) {
|
|
continue;
|
|
}
|
|
|
|
found = true;
|
|
debug("Using " + localConfigFile);
|
|
config = util.mergeConfigs(localConfig, config);
|
|
}
|
|
|
|
// Use the personal config file if there are no other local config files found.
|
|
return found ? config : util.mergeConfigs(config, getPersonalConfig());
|
|
}
|
|
|
|
/**
|
|
* Creates an environment config based on the specified environments.
|
|
* @param {Object<string,boolean>} envs The environment settings.
|
|
* @param {boolean} reset The value of the command line reset option. If true,
|
|
* rules are not automatically merged into the config.
|
|
* @returns {Object} A configuration object with the appropriate rules and globals
|
|
* set.
|
|
* @private
|
|
*/
|
|
function createEnvironmentConfig(envs, reset) {
|
|
|
|
var envConfig = {
|
|
globals: {},
|
|
env: envs || {},
|
|
rules: {},
|
|
ecmaFeatures: {}
|
|
};
|
|
|
|
if (envs) {
|
|
Object.keys(envs).filter(function (name) {
|
|
return envs[name];
|
|
}).forEach(function(name) {
|
|
var environment = environments[name];
|
|
|
|
if (environment) {
|
|
|
|
if (!reset && environment.rules) {
|
|
assign(envConfig.rules, environment.rules);
|
|
}
|
|
|
|
if (environment.globals) {
|
|
assign(envConfig.globals, environment.globals);
|
|
}
|
|
|
|
if (environment.ecmaFeatures) {
|
|
assign(envConfig.ecmaFeatures, environment.ecmaFeatures);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
return envConfig;
|
|
}
|
|
|
|
//------------------------------------------------------------------------------
|
|
// API
|
|
//------------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Config
|
|
* @constructor
|
|
* @class Config
|
|
* @param {Object} options Options to be passed in
|
|
* @param {string} [cwd] current working directory. Defaults to process.cwd()
|
|
*/
|
|
function Config(options) {
|
|
var useConfig;
|
|
|
|
options = options || {};
|
|
|
|
this.ignore = options.ignore;
|
|
this.ignorePath = options.ignorePath;
|
|
this.cache = {};
|
|
|
|
if (options.reset || options.baseConfig === false) {
|
|
// If `options.reset` is truthy or `options.baseConfig` is set to `false`,
|
|
// disable all default rules and environments
|
|
this.baseConfig = { rules: {} };
|
|
} else {
|
|
// If `options.baseConfig` is an object, just use it,
|
|
// otherwise use default base config from `conf/eslint.json`
|
|
this.baseConfig = options.baseConfig ||
|
|
require(path.resolve(__dirname, "..", "conf", "eslint.json"));
|
|
}
|
|
|
|
this.useEslintrc = (options.useEslintrc !== false);
|
|
|
|
this.env = (options.envs || []).reduce(function (envs, name) {
|
|
envs[name] = true;
|
|
return envs;
|
|
}, {});
|
|
|
|
this.globals = (options.globals || []).reduce(function (globals, def) {
|
|
// Default "foo" to false and handle "foo:false" and "foo:true"
|
|
var parts = def.split(":");
|
|
globals[parts[0]] = (parts.length > 1 && parts[1] === "true");
|
|
return globals;
|
|
}, {});
|
|
|
|
useConfig = options.configFile;
|
|
this.options = options;
|
|
|
|
if (useConfig) {
|
|
debug("Using command line config " + useConfig);
|
|
this.useSpecificConfig = loadConfig(path.resolve(process.cwd(), useConfig));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Build a config object merging the base config (conf/eslint.json), the
|
|
* environments config (conf/environments.js) and eventually the user config.
|
|
* @param {string} filePath a file in whose directory we start looking for a local config
|
|
* @returns {Object} config object
|
|
*/
|
|
Config.prototype.getConfig = function (filePath) {
|
|
var config,
|
|
userConfig,
|
|
directory = filePath ? path.dirname(filePath) : process.cwd(),
|
|
pluginConfig;
|
|
|
|
debug("Constructing config for " + (filePath ? filePath : "text"));
|
|
|
|
config = this.cache[directory];
|
|
|
|
if (config) {
|
|
debug("Using config from cache");
|
|
return config;
|
|
}
|
|
|
|
// Step 1: Determine user-specified config from .eslintrc and package.json files
|
|
if (this.useEslintrc) {
|
|
debug("Using .eslintrc and package.json files");
|
|
userConfig = getLocalConfig(this, directory);
|
|
} else {
|
|
debug("Not using .eslintrc or package.json files");
|
|
userConfig = {};
|
|
}
|
|
|
|
// Step 2: Create a copy of the baseConfig
|
|
config = util.mergeConfigs({}, this.baseConfig);
|
|
|
|
// Step 3: Merge in environment-specific globals and rules from .eslintrc files
|
|
config = util.mergeConfigs(config, createEnvironmentConfig(userConfig.env, this.options.reset));
|
|
|
|
// Step 4: Merge in the user-specified configuration from .eslintrc and package.json
|
|
config = util.mergeConfigs(config, userConfig);
|
|
|
|
// Step 5: Merge in command line config file
|
|
if (this.useSpecificConfig) {
|
|
debug("Merging command line config file");
|
|
|
|
if (this.useSpecificConfig.env) {
|
|
config = util.mergeConfigs(config, createEnvironmentConfig(this.useSpecificConfig.env, this.options.reset));
|
|
}
|
|
|
|
config = util.mergeConfigs(config, this.useSpecificConfig);
|
|
}
|
|
|
|
// Step 6: Merge in command line environments
|
|
debug("Merging command line environment settings");
|
|
config = util.mergeConfigs(config, createEnvironmentConfig(this.env, this.options.reset));
|
|
|
|
// Step 7: Merge in command line rules
|
|
if (this.options.rules) {
|
|
debug("Merging command line rules");
|
|
config = util.mergeConfigs(config, { rules: this.options.rules });
|
|
}
|
|
|
|
// Step 8: Merge in command line globals
|
|
config = util.mergeConfigs(config, { globals: this.globals });
|
|
|
|
|
|
// Step 9: Merge in plugin specific rules in reverse
|
|
if (config.plugins) {
|
|
pluginConfig = getPluginsConfig(config.plugins);
|
|
config = util.mergeConfigs(pluginConfig, config);
|
|
}
|
|
|
|
this.cache[directory] = config;
|
|
|
|
return config;
|
|
};
|
|
|
|
/**
|
|
* Find local config files from directory and parent directories.
|
|
* @param {string} directory The directory to start searching from.
|
|
* @returns {string[]} The paths of local config files found.
|
|
*/
|
|
Config.prototype.findLocalConfigFiles = function (directory) {
|
|
|
|
if (!this.localConfigFinder) {
|
|
this.localConfigFinder = new FileFinder(LOCAL_CONFIG_FILENAME, PACKAGE_CONFIG_FILENAME);
|
|
}
|
|
|
|
return this.localConfigFinder.findAllInDirectoryAndParents(directory);
|
|
};
|
|
|
|
module.exports = Config;
|
|
|