UNPKG

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