UNPKG

10.5 kBJavaScriptView Raw
1/**
2 * @fileoverview Responsible for loading config files
3 * @author Seth McLaughlin
4 */
5
6"use strict";
7
8//------------------------------------------------------------------------------
9// Requirements
10//------------------------------------------------------------------------------
11
12const path = require("path"),
13 ConfigOps = require("./config/config-ops"),
14 ConfigFile = require("./config/config-file"),
15 Plugins = require("./config/plugins"),
16 FileFinder = require("./file-finder"),
17 userHome = require("user-home"),
18 isResolvable = require("is-resolvable"),
19 pathIsInside = require("path-is-inside");
20
21const debug = require("debug")("eslint:config");
22
23//------------------------------------------------------------------------------
24// Constants
25//------------------------------------------------------------------------------
26
27const PERSONAL_CONFIG_DIR = userHome || null;
28
29//------------------------------------------------------------------------------
30// Helpers
31//------------------------------------------------------------------------------
32
33/**
34 * Check if item is an javascript object
35 * @param {*} item object to check for
36 * @returns {boolean} True if its an object
37 * @private
38 */
39function isObject(item) {
40 return typeof item === "object" && !Array.isArray(item) && item !== null;
41}
42
43/**
44 * Load and parse a JSON config object from a file.
45 * @param {string|Object} configToLoad the path to the JSON config file or the config object itself.
46 * @returns {Object} the parsed config object (empty object if there was a parse error)
47 * @private
48 */
49function loadConfig(configToLoad) {
50 let config = {},
51 filePath = "";
52
53 if (configToLoad) {
54
55 if (isObject(configToLoad)) {
56 config = configToLoad;
57
58 if (config.extends) {
59 config = ConfigFile.applyExtends(config, filePath);
60 }
61 } else {
62 filePath = configToLoad;
63 config = ConfigFile.load(filePath);
64 }
65
66 }
67
68 return config;
69}
70
71/**
72 * Get personal config object from ~/.eslintrc.
73 * @returns {Object} the personal config object (null if there is no personal config)
74 * @private
75 */
76function getPersonalConfig() {
77 let config;
78
79 if (PERSONAL_CONFIG_DIR) {
80 const filename = ConfigFile.getFilenameForDirectory(PERSONAL_CONFIG_DIR);
81
82 if (filename) {
83 debug("Using personal config");
84 config = loadConfig(filename);
85 }
86 }
87
88 return config || null;
89}
90
91/**
92 * Determine if rules were explicitly passed in as options.
93 * @param {Object} options The options used to create our configuration.
94 * @returns {boolean} True if rules were passed in as options, false otherwise.
95 */
96function hasRules(options) {
97 return options.rules && Object.keys(options.rules).length > 0;
98}
99
100/**
101 * Get a local config object.
102 * @param {Object} thisConfig A Config object.
103 * @param {string} directory The directory to start looking in for a local config file.
104 * @returns {Object} The local config object, or an empty object if there is no local config.
105 */
106function getLocalConfig(thisConfig, directory) {
107 const localConfigFiles = thisConfig.findLocalConfigFiles(directory),
108 numFiles = localConfigFiles.length,
109 projectConfigPath = ConfigFile.getFilenameForDirectory(thisConfig.options.cwd);
110 let found,
111 config = {},
112 rootPath;
113
114 for (let i = 0; i < numFiles; i++) {
115
116 const localConfigFile = localConfigFiles[i];
117
118 // Don't consider the personal config file in the home directory,
119 // except if the home directory is the same as the current working directory
120 if (path.dirname(localConfigFile) === PERSONAL_CONFIG_DIR && localConfigFile !== projectConfigPath) {
121 continue;
122 }
123
124 // If root flag is set, don't consider file if it is above root
125 if (rootPath && !pathIsInside(path.dirname(localConfigFile), rootPath)) {
126 continue;
127 }
128
129 debug("Loading " + localConfigFile);
130 const localConfig = loadConfig(localConfigFile);
131
132 // Don't consider a local config file found if the config is null
133 if (!localConfig) {
134 continue;
135 }
136
137 // Check for root flag
138 if (localConfig.root === true) {
139 rootPath = path.dirname(localConfigFile);
140 }
141
142 found = true;
143 debug("Using " + localConfigFile);
144 config = ConfigOps.merge(localConfig, config);
145 }
146
147 if (!found && !thisConfig.useSpecificConfig) {
148
149 /*
150 * - Is there a personal config in the user's home directory? If so,
151 * merge that with the passed-in config.
152 * - Otherwise, if no rules were manually passed in, throw and error.
153 * - Note: This function is not called if useEslintrc is false.
154 */
155 const personalConfig = getPersonalConfig();
156
157 if (personalConfig) {
158 config = ConfigOps.merge(config, personalConfig);
159 } else if (!hasRules(thisConfig.options) && !thisConfig.options.baseConfig) {
160
161 // No config file, no manual configuration, and no rules, so error.
162 const noConfigError = new Error("No ESLint configuration found.");
163
164 noConfigError.messageTemplate = "no-config-found";
165 noConfigError.messageData = {
166 directory,
167 filesExamined: localConfigFiles
168 };
169
170 throw noConfigError;
171 }
172 }
173
174 return config;
175}
176
177//------------------------------------------------------------------------------
178// API
179//------------------------------------------------------------------------------
180
181/**
182 * Config
183 * @constructor
184 * @class Config
185 * @param {Object} options Options to be passed in
186 */
187function Config(options) {
188 options = options || {};
189
190 this.ignore = options.ignore;
191 this.ignorePath = options.ignorePath;
192 this.cache = {};
193 this.parser = options.parser;
194 this.parserOptions = options.parserOptions || {};
195
196 this.baseConfig = options.baseConfig ? loadConfig(options.baseConfig) : { rules: {} };
197
198 this.useEslintrc = (options.useEslintrc !== false);
199
200 this.env = (options.envs || []).reduce(function(envs, name) {
201 envs[name] = true;
202 return envs;
203 }, {});
204
205 /*
206 * Handle declared globals.
207 * For global variable foo, handle "foo:false" and "foo:true" to set
208 * whether global is writable.
209 * If user declares "foo", convert to "foo:false".
210 */
211 this.globals = (options.globals || []).reduce(function(globals, def) {
212 const parts = def.split(":");
213
214 globals[parts[0]] = (parts.length > 1 && parts[1] === "true");
215
216 return globals;
217 }, {});
218
219 const useConfig = options.configFile;
220
221 this.options = options;
222
223 if (useConfig) {
224 debug("Using command line config " + useConfig);
225 if (isResolvable(useConfig) || isResolvable("eslint-config-" + useConfig) || useConfig.charAt(0) === "@") {
226 this.useSpecificConfig = loadConfig(useConfig);
227 } else {
228 this.useSpecificConfig = loadConfig(path.resolve(this.options.cwd, useConfig));
229 }
230 }
231}
232
233/**
234 * Build a config object merging the base config (conf/eslint.json), the
235 * environments config (conf/environments.js) and eventually the user config.
236 * @param {string} filePath a file in whose directory we start looking for a local config
237 * @returns {Object} config object
238 */
239Config.prototype.getConfig = function(filePath) {
240 const directory = filePath ? path.dirname(filePath) : this.options.cwd;
241 let config,
242 userConfig;
243
244 debug("Constructing config for " + (filePath ? filePath : "text"));
245
246 config = this.cache[directory];
247
248 if (config) {
249 debug("Using config from cache");
250 return config;
251 }
252
253 // Step 1: Determine user-specified config from .eslintrc.* and package.json files
254 if (this.useEslintrc) {
255 debug("Using .eslintrc and package.json files");
256 userConfig = getLocalConfig(this, directory);
257 } else {
258 debug("Not using .eslintrc or package.json files");
259 userConfig = {};
260 }
261
262 // Step 2: Create a copy of the baseConfig
263 config = ConfigOps.merge({}, this.baseConfig);
264
265 // Step 3: Merge in the user-specified configuration from .eslintrc and package.json
266 config = ConfigOps.merge(config, userConfig);
267
268 // Step 4: Merge in command line config file
269 if (this.useSpecificConfig) {
270 debug("Merging command line config file");
271
272 config = ConfigOps.merge(config, this.useSpecificConfig);
273 }
274
275 // Step 5: Merge in command line environments
276 debug("Merging command line environment settings");
277 config = ConfigOps.merge(config, { env: this.env });
278
279 // Step 6: Merge in command line rules
280 if (this.options.rules) {
281 debug("Merging command line rules");
282 config = ConfigOps.merge(config, { rules: this.options.rules });
283 }
284
285 // Step 7: Merge in command line globals
286 config = ConfigOps.merge(config, { globals: this.globals });
287
288 // Only override parser if it is passed explicitly through the command line or if it's not
289 // defined yet (because the final object will at least have the parser key)
290 if (this.parser || !config.parser) {
291 config = ConfigOps.merge(config, {
292 parser: this.parser
293 });
294 }
295
296 if (this.parserOptions) {
297 config = ConfigOps.merge(config, {
298 parserOptions: this.parserOptions
299 });
300 }
301
302 // Step 8: Merge in command line plugins
303 if (this.options.plugins) {
304 debug("Merging command line plugins");
305 Plugins.loadAll(this.options.plugins);
306 config = ConfigOps.merge(config, { plugins: this.options.plugins });
307 }
308
309 // Step 9: Apply environments to the config if present
310 if (config.env) {
311 config = ConfigOps.applyEnvironments(config);
312 }
313
314 this.cache[directory] = config;
315
316 return config;
317};
318
319/**
320 * Find local config files from directory and parent directories.
321 * @param {string} directory The directory to start searching from.
322 * @returns {string[]} The paths of local config files found.
323 */
324Config.prototype.findLocalConfigFiles = function(directory) {
325
326 if (!this.localConfigFinder) {
327 this.localConfigFinder = new FileFinder(ConfigFile.CONFIG_FILES, this.options.cwd);
328 }
329
330 return this.localConfigFinder.findAllInDirectoryAndParents(directory);
331};
332
333module.exports = Config;