1 | /**
|
2 | * @fileoverview `CascadingConfigArrayFactory` class.
|
3 | *
|
4 | * `CascadingConfigArrayFactory` class has a responsibility:
|
5 | *
|
6 | * 1. Handles cascading of config files.
|
7 | *
|
8 | * It provides two methods:
|
9 | *
|
10 | * - `getConfigArrayForFile(filePath)`
|
11 | * Get the corresponded configuration of a given file. This method doesn't
|
12 | * throw even if the given file didn't exist.
|
13 | * - `clearCache()`
|
14 | * Clear the internal cache. You have to call this method when
|
15 | * `additionalPluginPool` was updated if `baseConfig` or `cliConfig` depends
|
16 | * on the additional plugins. (`CLIEngine#addPlugin()` method calls this.)
|
17 | *
|
18 | * @author Toru Nagashima <https://github.com/mysticatea>
|
19 | */
|
20 | ;
|
21 |
|
22 | //------------------------------------------------------------------------------
|
23 | // Requirements
|
24 | //------------------------------------------------------------------------------
|
25 |
|
26 | const os = require("os");
|
27 | const path = require("path");
|
28 | const { validateConfigArray } = require("../shared/config-validator");
|
29 | const { ConfigArrayFactory } = require("./config-array-factory");
|
30 | const { ConfigArray, ConfigDependency } = require("./config-array");
|
31 | const loadRules = require("./load-rules");
|
32 | const debug = require("debug")("eslint:cascading-config-array-factory");
|
33 |
|
34 | //------------------------------------------------------------------------------
|
35 | // Helpers
|
36 | //------------------------------------------------------------------------------
|
37 |
|
38 | // Define types for VSCode IntelliSense.
|
39 | /** @typedef {import("../shared/types").ConfigData} ConfigData */
|
40 | /** @typedef {import("../shared/types").Parser} Parser */
|
41 | /** @typedef {import("../shared/types").Plugin} Plugin */
|
42 | /** @typedef {ReturnType<ConfigArrayFactory["create"]>} ConfigArray */
|
43 |
|
44 | /**
|
45 | * @typedef {Object} CascadingConfigArrayFactoryOptions
|
46 | * @property {Map<string,Plugin>} [additionalPluginPool] The map for additional plugins.
|
47 | * @property {ConfigData} [baseConfig] The config by `baseConfig` option.
|
48 | * @property {ConfigData} [cliConfig] The config by CLI options (`--env`, `--global`, `--parser`, `--parser-options`, `--plugin`, and `--rule`). CLI options overwrite the setting in config files.
|
49 | * @property {string} [cwd] The base directory to start lookup.
|
50 | * @property {string[]} [rulePaths] The value of `--rulesdir` option.
|
51 | * @property {string} [specificConfigPath] The value of `--config` option.
|
52 | * @property {boolean} [useEslintrc] if `false` then it doesn't load config files.
|
53 | */
|
54 |
|
55 | /**
|
56 | * @typedef {Object} CascadingConfigArrayFactoryInternalSlots
|
57 | * @property {ConfigArray} baseConfigArray The config array of `baseConfig` option.
|
58 | * @property {ConfigData} baseConfigData The config data of `baseConfig` option. This is used to reset `baseConfigArray`.
|
59 | * @property {ConfigArray} cliConfigArray The config array of CLI options.
|
60 | * @property {ConfigData} cliConfigData The config data of CLI options. This is used to reset `cliConfigArray`.
|
61 | * @property {ConfigArrayFactory} configArrayFactory The factory for config arrays.
|
62 | * @property {Map<string, ConfigArray>} configCache The cache from directory paths to config arrays.
|
63 | * @property {string} cwd The base directory to start lookup.
|
64 | * @property {WeakMap<ConfigArray, ConfigArray>} finalizeCache The cache from config arrays to finalized config arrays.
|
65 | * @property {string[]|null} rulePaths The value of `--rulesdir` option. This is used to reset `baseConfigArray`.
|
66 | * @property {string|null} specificConfigPath The value of `--config` option. This is used to reset `cliConfigArray`.
|
67 | * @property {boolean} useEslintrc if `false` then it doesn't load config files.
|
68 | */
|
69 |
|
70 | /** @type {WeakMap<CascadingConfigArrayFactory, CascadingConfigArrayFactoryInternalSlots>} */
|
71 | const internalSlotsMap = new WeakMap();
|
72 |
|
73 | /**
|
74 | * Create the config array from `baseConfig` and `rulePaths`.
|
75 | * @param {CascadingConfigArrayFactoryInternalSlots} slots The slots.
|
76 | * @returns {ConfigArray} The config array of the base configs.
|
77 | */
|
78 | function createBaseConfigArray({
|
79 | configArrayFactory,
|
80 | baseConfigData,
|
81 | rulePaths,
|
82 | cwd
|
83 | }) {
|
84 | const baseConfigArray = configArrayFactory.create(
|
85 | baseConfigData,
|
86 | { name: "BaseConfig" }
|
87 | );
|
88 |
|
89 | if (rulePaths && rulePaths.length > 0) {
|
90 |
|
91 | /*
|
92 | * Load rules `--rulesdir` option as a pseudo plugin.
|
93 | * Use a pseudo plugin to define rules of `--rulesdir`, so we can
|
94 | * validate the rule's options with only information in the config
|
95 | * array.
|
96 | */
|
97 | baseConfigArray.push({
|
98 | name: "--rulesdir",
|
99 | filePath: "",
|
100 | plugins: {
|
101 | "": new ConfigDependency({
|
102 | definition: {
|
103 | rules: rulePaths.reduce(
|
104 | (map, rulesPath) => Object.assign(
|
105 | map,
|
106 | loadRules(rulesPath, cwd)
|
107 | ),
|
108 | {}
|
109 | )
|
110 | },
|
111 | filePath: "",
|
112 | id: "",
|
113 | importerName: "--rulesdir",
|
114 | importerPath: ""
|
115 | })
|
116 | }
|
117 | });
|
118 | }
|
119 |
|
120 | return baseConfigArray;
|
121 | }
|
122 |
|
123 | /**
|
124 | * Create the config array from CLI options.
|
125 | * @param {CascadingConfigArrayFactoryInternalSlots} slots The slots.
|
126 | * @returns {ConfigArray} The config array of the base configs.
|
127 | */
|
128 | function createCLIConfigArray({
|
129 | cliConfigData,
|
130 | configArrayFactory,
|
131 | specificConfigPath
|
132 | }) {
|
133 | const cliConfigArray = configArrayFactory.create(
|
134 | cliConfigData,
|
135 | { name: "CLIOptions" }
|
136 | );
|
137 |
|
138 | if (specificConfigPath) {
|
139 | cliConfigArray.unshift(
|
140 | ...configArrayFactory.loadFile(
|
141 | specificConfigPath,
|
142 | { name: "--config" }
|
143 | )
|
144 | );
|
145 | }
|
146 |
|
147 | return cliConfigArray;
|
148 | }
|
149 |
|
150 | /**
|
151 | * The error type when there are files matched by a glob, but all of them have been ignored.
|
152 | */
|
153 | class ConfigurationNotFoundError extends Error {
|
154 |
|
155 | /**
|
156 | * @param {string} directoryPath - The directory path.
|
157 | */
|
158 | constructor(directoryPath) {
|
159 | super(`No ESLint configuration found in ${directoryPath}.`);
|
160 | this.messageTemplate = "no-config-found";
|
161 | this.messageData = { directoryPath };
|
162 | }
|
163 | }
|
164 |
|
165 | /**
|
166 | * This class provides the functionality that enumerates every file which is
|
167 | * matched by given glob patterns and that configuration.
|
168 | */
|
169 | class CascadingConfigArrayFactory {
|
170 |
|
171 | /**
|
172 | * Initialize this enumerator.
|
173 | * @param {CascadingConfigArrayFactoryOptions} options The options.
|
174 | */
|
175 | constructor({
|
176 | additionalPluginPool = new Map(),
|
177 | baseConfig: baseConfigData = null,
|
178 | cliConfig: cliConfigData = null,
|
179 | cwd = process.cwd(),
|
180 | resolvePluginsRelativeTo = cwd,
|
181 | rulePaths = [],
|
182 | specificConfigPath = null,
|
183 | useEslintrc = true
|
184 | } = {}) {
|
185 | const configArrayFactory = new ConfigArrayFactory({
|
186 | additionalPluginPool,
|
187 | cwd,
|
188 | resolvePluginsRelativeTo
|
189 | });
|
190 |
|
191 | internalSlotsMap.set(this, {
|
192 | baseConfigArray: createBaseConfigArray({
|
193 | baseConfigData,
|
194 | configArrayFactory,
|
195 | cwd,
|
196 | rulePaths
|
197 | }),
|
198 | baseConfigData,
|
199 | cliConfigArray: createCLIConfigArray({
|
200 | cliConfigData,
|
201 | configArrayFactory,
|
202 | specificConfigPath
|
203 | }),
|
204 | cliConfigData,
|
205 | configArrayFactory,
|
206 | configCache: new Map(),
|
207 | cwd,
|
208 | finalizeCache: new WeakMap(),
|
209 | rulePaths,
|
210 | specificConfigPath,
|
211 | useEslintrc
|
212 | });
|
213 | }
|
214 |
|
215 | /**
|
216 | * The path to the current working directory.
|
217 | * This is used by tests.
|
218 | * @type {string}
|
219 | */
|
220 | get cwd() {
|
221 | const { cwd } = internalSlotsMap.get(this);
|
222 |
|
223 | return cwd;
|
224 | }
|
225 |
|
226 | /**
|
227 | * Get the config array of a given file.
|
228 | * If `filePath` was not given, it returns the config which contains only
|
229 | * `baseConfigData` and `cliConfigData`.
|
230 | * @param {string} [filePath] The file path to a file.
|
231 | * @returns {ConfigArray} The config array of the file.
|
232 | */
|
233 | getConfigArrayForFile(filePath) {
|
234 | const {
|
235 | baseConfigArray,
|
236 | cliConfigArray,
|
237 | cwd
|
238 | } = internalSlotsMap.get(this);
|
239 |
|
240 | if (!filePath) {
|
241 | return new ConfigArray(...baseConfigArray, ...cliConfigArray);
|
242 | }
|
243 |
|
244 | const directoryPath = path.dirname(path.resolve(cwd, filePath));
|
245 |
|
246 | debug(`Load config files for ${directoryPath}.`);
|
247 |
|
248 | return this._finalizeConfigArray(
|
249 | this._loadConfigInAncestors(directoryPath),
|
250 | directoryPath
|
251 | );
|
252 | }
|
253 |
|
254 | /**
|
255 | * Clear config cache.
|
256 | * @returns {void}
|
257 | */
|
258 | clearCache() {
|
259 | const slots = internalSlotsMap.get(this);
|
260 |
|
261 | slots.baseConfigArray = createBaseConfigArray(slots);
|
262 | slots.cliConfigArray = createCLIConfigArray(slots);
|
263 | slots.configCache.clear();
|
264 | }
|
265 |
|
266 | /**
|
267 | * Load and normalize config files from the ancestor directories.
|
268 | * @param {string} directoryPath The path to a leaf directory.
|
269 | * @returns {ConfigArray} The loaded config.
|
270 | * @private
|
271 | */
|
272 | _loadConfigInAncestors(directoryPath) {
|
273 | const {
|
274 | baseConfigArray,
|
275 | configArrayFactory,
|
276 | configCache,
|
277 | cwd,
|
278 | useEslintrc
|
279 | } = internalSlotsMap.get(this);
|
280 |
|
281 | if (!useEslintrc) {
|
282 | return baseConfigArray;
|
283 | }
|
284 |
|
285 | let configArray = configCache.get(directoryPath);
|
286 |
|
287 | // Hit cache.
|
288 | if (configArray) {
|
289 | debug(`Cache hit: ${directoryPath}.`);
|
290 | return configArray;
|
291 | }
|
292 | debug(`No cache found: ${directoryPath}.`);
|
293 |
|
294 | const homePath = os.homedir();
|
295 |
|
296 | // Consider this is root.
|
297 | if (directoryPath === homePath && cwd !== homePath) {
|
298 | debug("Stop traversing because of considered root.");
|
299 | return this._cacheConfig(directoryPath, baseConfigArray);
|
300 | }
|
301 |
|
302 | // Load the config on this directory.
|
303 | try {
|
304 | configArray = configArrayFactory.loadInDirectory(directoryPath);
|
305 | } catch (error) {
|
306 | /* istanbul ignore next */
|
307 | if (error.code === "EACCES") {
|
308 | debug("Stop traversing because of 'EACCES' error.");
|
309 | return this._cacheConfig(directoryPath, baseConfigArray);
|
310 | }
|
311 | throw error;
|
312 | }
|
313 |
|
314 | if (configArray.length > 0 && configArray.isRoot()) {
|
315 | debug("Stop traversing because of 'root:true'.");
|
316 | configArray.unshift(...baseConfigArray);
|
317 | return this._cacheConfig(directoryPath, configArray);
|
318 | }
|
319 |
|
320 | // Load from the ancestors and merge it.
|
321 | const parentPath = path.dirname(directoryPath);
|
322 | const parentConfigArray = parentPath && parentPath !== directoryPath
|
323 | ? this._loadConfigInAncestors(parentPath)
|
324 | : baseConfigArray;
|
325 |
|
326 | if (configArray.length > 0) {
|
327 | configArray.unshift(...parentConfigArray);
|
328 | } else {
|
329 | configArray = parentConfigArray;
|
330 | }
|
331 |
|
332 | // Cache and return.
|
333 | return this._cacheConfig(directoryPath, configArray);
|
334 | }
|
335 |
|
336 | /**
|
337 | * Freeze and cache a given config.
|
338 | * @param {string} directoryPath The path to a directory as a cache key.
|
339 | * @param {ConfigArray} configArray The config array as a cache value.
|
340 | * @returns {ConfigArray} The `configArray` (frozen).
|
341 | */
|
342 | _cacheConfig(directoryPath, configArray) {
|
343 | const { configCache } = internalSlotsMap.get(this);
|
344 |
|
345 | Object.freeze(configArray);
|
346 | configCache.set(directoryPath, configArray);
|
347 |
|
348 | return configArray;
|
349 | }
|
350 |
|
351 | /**
|
352 | * Finalize a given config array.
|
353 | * Concatenate `--config` and other CLI options.
|
354 | * @param {ConfigArray} configArray The parent config array.
|
355 | * @param {string} directoryPath The path to the leaf directory to find config files.
|
356 | * @returns {ConfigArray} The loaded config.
|
357 | * @private
|
358 | */
|
359 | _finalizeConfigArray(configArray, directoryPath) {
|
360 | const {
|
361 | cliConfigArray,
|
362 | configArrayFactory,
|
363 | finalizeCache,
|
364 | useEslintrc
|
365 | } = internalSlotsMap.get(this);
|
366 |
|
367 | let finalConfigArray = finalizeCache.get(configArray);
|
368 |
|
369 | if (!finalConfigArray) {
|
370 | finalConfigArray = configArray;
|
371 |
|
372 | // Load the personal config if there are no regular config files.
|
373 | if (
|
374 | useEslintrc &&
|
375 | configArray.every(c => !c.filePath) &&
|
376 | cliConfigArray.every(c => !c.filePath) // `--config` option can be a file.
|
377 | ) {
|
378 | debug("Loading the config file of the home directory.");
|
379 |
|
380 | finalConfigArray = configArrayFactory.loadInDirectory(
|
381 | os.homedir(),
|
382 | { name: "PersonalConfig", parent: finalConfigArray }
|
383 | );
|
384 | }
|
385 |
|
386 | // Apply CLI options.
|
387 | if (cliConfigArray.length > 0) {
|
388 | finalConfigArray = finalConfigArray.concat(cliConfigArray);
|
389 | }
|
390 |
|
391 | // Validate rule settings and environments.
|
392 | validateConfigArray(finalConfigArray);
|
393 |
|
394 | // Cache it.
|
395 | Object.freeze(finalConfigArray);
|
396 | finalizeCache.set(configArray, finalConfigArray);
|
397 |
|
398 | debug(
|
399 | "Configuration was determined: %o on %s",
|
400 | finalConfigArray,
|
401 | directoryPath
|
402 | );
|
403 | }
|
404 |
|
405 | if (useEslintrc && finalConfigArray.length === 0) {
|
406 | throw new ConfigurationNotFoundError(directoryPath);
|
407 | }
|
408 |
|
409 | return finalConfigArray;
|
410 | }
|
411 | }
|
412 |
|
413 | //------------------------------------------------------------------------------
|
414 | // Public Interface
|
415 | //------------------------------------------------------------------------------
|
416 |
|
417 | module.exports = { CascadingConfigArrayFactory };
|