1 | /**
|
2 | * @fileoverview The factory of `ConfigArray` objects.
|
3 | *
|
4 | * This class provides methods to create `ConfigArray` instance.
|
5 | *
|
6 | * - `create(configData, options)`
|
7 | * Create a `ConfigArray` instance from a config data. This is to handle CLI
|
8 | * options except `--config`.
|
9 | * - `loadFile(filePath, options)`
|
10 | * Create a `ConfigArray` instance from a config file. This is to handle
|
11 | * `--config` option. If the file was not found, throws the following error:
|
12 | * - If the filename was `*.js`, a `MODULE_NOT_FOUND` error.
|
13 | * - If the filename was `package.json`, an IO error or an
|
14 | * `ESLINT_CONFIG_FIELD_NOT_FOUND` error.
|
15 | * - Otherwise, an IO error such as `ENOENT`.
|
16 | * - `loadInDirectory(directoryPath, options)`
|
17 | * Create a `ConfigArray` instance from a config file which is on a given
|
18 | * directory. This tries to load `.eslintrc.*` or `package.json`. If not
|
19 | * found, returns an empty `ConfigArray`.
|
20 | *
|
21 | * `ConfigArrayFactory` class has the responsibility that loads configuration
|
22 | * files, including loading `extends`, `parser`, and `plugins`. The created
|
23 | * `ConfigArray` instance has the loaded `extends`, `parser`, and `plugins`.
|
24 | *
|
25 | * But this class doesn't handle cascading. `CascadingConfigArrayFactory` class
|
26 | * handles cascading and hierarchy.
|
27 | *
|
28 | * @author Toru Nagashima <https://github.com/mysticatea>
|
29 | */
|
30 | ;
|
31 |
|
32 | //------------------------------------------------------------------------------
|
33 | // Requirements
|
34 | //------------------------------------------------------------------------------
|
35 |
|
36 | const fs = require("fs");
|
37 | const path = require("path");
|
38 | const importFresh = require("import-fresh");
|
39 | const stripComments = require("strip-json-comments");
|
40 | const { validateConfigSchema } = require("../shared/config-validator");
|
41 | const naming = require("../shared/naming");
|
42 | const ModuleResolver = require("../shared/relative-module-resolver");
|
43 | const { ConfigArray, ConfigDependency, OverrideTester } = require("./config-array");
|
44 | const debug = require("debug")("eslint:config-array-factory");
|
45 |
|
46 | //------------------------------------------------------------------------------
|
47 | // Helpers
|
48 | //------------------------------------------------------------------------------
|
49 |
|
50 | const eslintRecommendedPath = path.resolve(__dirname, "../../conf/eslint-recommended.js");
|
51 | const eslintAllPath = path.resolve(__dirname, "../../conf/eslint-all.js");
|
52 | const configFilenames = [
|
53 | ".eslintrc.js",
|
54 | ".eslintrc.yaml",
|
55 | ".eslintrc.yml",
|
56 | ".eslintrc.json",
|
57 | ".eslintrc",
|
58 | "package.json"
|
59 | ];
|
60 |
|
61 | // Define types for VSCode IntelliSense.
|
62 | /** @typedef {import("../shared/types").ConfigData} ConfigData */
|
63 | /** @typedef {import("../shared/types").OverrideConfigData} OverrideConfigData */
|
64 | /** @typedef {import("../shared/types").Parser} Parser */
|
65 | /** @typedef {import("../shared/types").Plugin} Plugin */
|
66 | /** @typedef {import("./config-array/config-dependency").DependentParser} DependentParser */
|
67 | /** @typedef {import("./config-array/config-dependency").DependentPlugin} DependentPlugin */
|
68 | /** @typedef {ConfigArray[0]} ConfigArrayElement */
|
69 |
|
70 | /**
|
71 | * @typedef {Object} ConfigArrayFactoryOptions
|
72 | * @property {Map<string,Plugin>} [additionalPluginPool] The map for additional plugins.
|
73 | * @property {string} [cwd] The path to the current working directory.
|
74 | * @property {string} [resolvePluginsRelativeTo] A path to the directory that plugins should be resolved from. Defaults to `cwd`.
|
75 | */
|
76 |
|
77 | /**
|
78 | * @typedef {Object} ConfigArrayFactoryInternalSlots
|
79 | * @property {Map<string,Plugin>} additionalPluginPool The map for additional plugins.
|
80 | * @property {string} cwd The path to the current working directory.
|
81 | * @property {string} resolvePluginsRelativeTo An absolute path the the directory that plugins should be resolved from.
|
82 | */
|
83 |
|
84 | /** @type {WeakMap<ConfigArrayFactory, ConfigArrayFactoryInternalSlots>} */
|
85 | const internalSlotsMap = new WeakMap();
|
86 |
|
87 | /**
|
88 | * Check if a given string is a file path.
|
89 | * @param {string} nameOrPath A module name or file path.
|
90 | * @returns {boolean} `true` if the `nameOrPath` is a file path.
|
91 | */
|
92 | function isFilePath(nameOrPath) {
|
93 | return (
|
94 | /^\.{1,2}[/\\]/u.test(nameOrPath) ||
|
95 | path.isAbsolute(nameOrPath)
|
96 | );
|
97 | }
|
98 |
|
99 | /**
|
100 | * Convenience wrapper for synchronously reading file contents.
|
101 | * @param {string} filePath The filename to read.
|
102 | * @returns {string} The file contents, with the BOM removed.
|
103 | * @private
|
104 | */
|
105 | function readFile(filePath) {
|
106 | return fs.readFileSync(filePath, "utf8").replace(/^\ufeff/u, "");
|
107 | }
|
108 |
|
109 | /**
|
110 | * Loads a YAML configuration from a file.
|
111 | * @param {string} filePath The filename to load.
|
112 | * @returns {ConfigData} The configuration object from the file.
|
113 | * @throws {Error} If the file cannot be read.
|
114 | * @private
|
115 | */
|
116 | function loadYAMLConfigFile(filePath) {
|
117 | debug(`Loading YAML config file: ${filePath}`);
|
118 |
|
119 | // lazy load YAML to improve performance when not used
|
120 | const yaml = require("js-yaml");
|
121 |
|
122 | try {
|
123 |
|
124 | // empty YAML file can be null, so always use
|
125 | return yaml.safeLoad(readFile(filePath)) || {};
|
126 | } catch (e) {
|
127 | debug(`Error reading YAML file: ${filePath}`);
|
128 | e.message = `Cannot read config file: ${filePath}\nError: ${e.message}`;
|
129 | throw e;
|
130 | }
|
131 | }
|
132 |
|
133 | /**
|
134 | * Loads a JSON configuration from a file.
|
135 | * @param {string} filePath The filename to load.
|
136 | * @returns {ConfigData} The configuration object from the file.
|
137 | * @throws {Error} If the file cannot be read.
|
138 | * @private
|
139 | */
|
140 | function loadJSONConfigFile(filePath) {
|
141 | debug(`Loading JSON config file: ${filePath}`);
|
142 |
|
143 | try {
|
144 | return JSON.parse(stripComments(readFile(filePath)));
|
145 | } catch (e) {
|
146 | debug(`Error reading JSON file: ${filePath}`);
|
147 | e.message = `Cannot read config file: ${filePath}\nError: ${e.message}`;
|
148 | e.messageTemplate = "failed-to-read-json";
|
149 | e.messageData = {
|
150 | path: filePath,
|
151 | message: e.message
|
152 | };
|
153 | throw e;
|
154 | }
|
155 | }
|
156 |
|
157 | /**
|
158 | * Loads a legacy (.eslintrc) configuration from a file.
|
159 | * @param {string} filePath The filename to load.
|
160 | * @returns {ConfigData} The configuration object from the file.
|
161 | * @throws {Error} If the file cannot be read.
|
162 | * @private
|
163 | */
|
164 | function loadLegacyConfigFile(filePath) {
|
165 | debug(`Loading legacy config file: ${filePath}`);
|
166 |
|
167 | // lazy load YAML to improve performance when not used
|
168 | const yaml = require("js-yaml");
|
169 |
|
170 | try {
|
171 | return yaml.safeLoad(stripComments(readFile(filePath))) || /* istanbul ignore next */ {};
|
172 | } catch (e) {
|
173 | debug("Error reading YAML file: %s\n%o", filePath, e);
|
174 | e.message = `Cannot read config file: ${filePath}\nError: ${e.message}`;
|
175 | throw e;
|
176 | }
|
177 | }
|
178 |
|
179 | /**
|
180 | * Loads a JavaScript configuration from a file.
|
181 | * @param {string} filePath The filename to load.
|
182 | * @returns {ConfigData} The configuration object from the file.
|
183 | * @throws {Error} If the file cannot be read.
|
184 | * @private
|
185 | */
|
186 | function loadJSConfigFile(filePath) {
|
187 | debug(`Loading JS config file: ${filePath}`);
|
188 | try {
|
189 | return importFresh(filePath);
|
190 | } catch (e) {
|
191 | debug(`Error reading JavaScript file: ${filePath}`);
|
192 | e.message = `Cannot read config file: ${filePath}\nError: ${e.message}`;
|
193 | throw e;
|
194 | }
|
195 | }
|
196 |
|
197 | /**
|
198 | * Loads a configuration from a package.json file.
|
199 | * @param {string} filePath The filename to load.
|
200 | * @returns {ConfigData} The configuration object from the file.
|
201 | * @throws {Error} If the file cannot be read.
|
202 | * @private
|
203 | */
|
204 | function loadPackageJSONConfigFile(filePath) {
|
205 | debug(`Loading package.json config file: ${filePath}`);
|
206 | try {
|
207 | const packageData = loadJSONConfigFile(filePath);
|
208 |
|
209 | if (!Object.hasOwnProperty.call(packageData, "eslintConfig")) {
|
210 | throw Object.assign(
|
211 | new Error("package.json file doesn't have 'eslintConfig' field."),
|
212 | { code: "ESLINT_CONFIG_FIELD_NOT_FOUND" }
|
213 | );
|
214 | }
|
215 |
|
216 | return packageData.eslintConfig;
|
217 | } catch (e) {
|
218 | debug(`Error reading package.json file: ${filePath}`);
|
219 | e.message = `Cannot read config file: ${filePath}\nError: ${e.message}`;
|
220 | throw e;
|
221 | }
|
222 | }
|
223 |
|
224 | /**
|
225 | * Creates an error to notify about a missing config to extend from.
|
226 | * @param {string} configName The name of the missing config.
|
227 | * @param {string} importerName The name of the config that imported the missing config
|
228 | * @returns {Error} The error object to throw
|
229 | * @private
|
230 | */
|
231 | function configMissingError(configName, importerName) {
|
232 | return Object.assign(
|
233 | new Error(`Failed to load config "${configName}" to extend from.`),
|
234 | {
|
235 | messageTemplate: "extend-config-missing",
|
236 | messageData: { configName, importerName }
|
237 | }
|
238 | );
|
239 | }
|
240 |
|
241 | /**
|
242 | * Loads a configuration file regardless of the source. Inspects the file path
|
243 | * to determine the correctly way to load the config file.
|
244 | * @param {string} filePath The path to the configuration.
|
245 | * @returns {ConfigData|null} The configuration information.
|
246 | * @private
|
247 | */
|
248 | function loadConfigFile(filePath) {
|
249 | switch (path.extname(filePath)) {
|
250 | case ".js":
|
251 | return loadJSConfigFile(filePath);
|
252 |
|
253 | case ".json":
|
254 | if (path.basename(filePath) === "package.json") {
|
255 | return loadPackageJSONConfigFile(filePath);
|
256 | }
|
257 | return loadJSONConfigFile(filePath);
|
258 |
|
259 | case ".yaml":
|
260 | case ".yml":
|
261 | return loadYAMLConfigFile(filePath);
|
262 |
|
263 | default:
|
264 | return loadLegacyConfigFile(filePath);
|
265 | }
|
266 | }
|
267 |
|
268 | /**
|
269 | * Write debug log.
|
270 | * @param {string} request The requested module name.
|
271 | * @param {string} relativeTo The file path to resolve the request relative to.
|
272 | * @param {string} filePath The resolved file path.
|
273 | * @returns {void}
|
274 | */
|
275 | function writeDebugLogForLoading(request, relativeTo, filePath) {
|
276 | /* istanbul ignore next */
|
277 | if (debug.enabled) {
|
278 | let nameAndVersion = null;
|
279 |
|
280 | try {
|
281 | const packageJsonPath = ModuleResolver.resolve(
|
282 | `${request}/package.json`,
|
283 | relativeTo
|
284 | );
|
285 | const { version = "unknown" } = require(packageJsonPath);
|
286 |
|
287 | nameAndVersion = `${request}@${version}`;
|
288 | } catch (error) {
|
289 | debug("package.json was not found:", error.message);
|
290 | nameAndVersion = request;
|
291 | }
|
292 |
|
293 | debug("Loaded: %s (%s)", nameAndVersion, filePath);
|
294 | }
|
295 | }
|
296 |
|
297 | /**
|
298 | * Concatenate two config data.
|
299 | * @param {IterableIterator<ConfigArrayElement>|null} elements The config elements.
|
300 | * @param {ConfigArray|null} parentConfigArray The parent config array.
|
301 | * @returns {ConfigArray} The concatenated config array.
|
302 | */
|
303 | function createConfigArray(elements, parentConfigArray) {
|
304 | if (!elements) {
|
305 | return parentConfigArray || new ConfigArray();
|
306 | }
|
307 | const configArray = new ConfigArray(...elements);
|
308 |
|
309 | if (parentConfigArray && !configArray.isRoot()) {
|
310 | configArray.unshift(...parentConfigArray);
|
311 | }
|
312 | return configArray;
|
313 | }
|
314 |
|
315 | /**
|
316 | * Normalize a given plugin.
|
317 | * - Ensure the object to have four properties: configs, environments, processors, and rules.
|
318 | * - Ensure the object to not have other properties.
|
319 | * @param {Plugin} plugin The plugin to normalize.
|
320 | * @returns {Plugin} The normalized plugin.
|
321 | */
|
322 | function normalizePlugin(plugin) {
|
323 | return {
|
324 | configs: plugin.configs || {},
|
325 | environments: plugin.environments || {},
|
326 | processors: plugin.processors || {},
|
327 | rules: plugin.rules || {}
|
328 | };
|
329 | }
|
330 |
|
331 | //------------------------------------------------------------------------------
|
332 | // Public Interface
|
333 | //------------------------------------------------------------------------------
|
334 |
|
335 | /**
|
336 | * The factory of `ConfigArray` objects.
|
337 | */
|
338 | class ConfigArrayFactory {
|
339 |
|
340 | /**
|
341 | * Initialize this instance.
|
342 | * @param {ConfigArrayFactoryOptions} [options] The map for additional plugins.
|
343 | */
|
344 | constructor({
|
345 | additionalPluginPool = new Map(),
|
346 | cwd = process.cwd(),
|
347 | resolvePluginsRelativeTo = cwd
|
348 | } = {}) {
|
349 | internalSlotsMap.set(this, { additionalPluginPool, cwd, resolvePluginsRelativeTo: path.resolve(cwd, resolvePluginsRelativeTo) });
|
350 | }
|
351 |
|
352 | /**
|
353 | * Create `ConfigArray` instance from a config data.
|
354 | * @param {ConfigData|null} configData The config data to create.
|
355 | * @param {Object} [options] The options.
|
356 | * @param {string} [options.filePath] The path to this config data.
|
357 | * @param {string} [options.name] The config name.
|
358 | * @param {ConfigArray} [options.parent] The parent config array.
|
359 | * @returns {ConfigArray} Loaded config.
|
360 | */
|
361 | create(configData, { filePath, name, parent } = {}) {
|
362 | return createConfigArray(
|
363 | configData
|
364 | ? this._normalizeConfigData(configData, filePath, name)
|
365 | : null,
|
366 | parent
|
367 | );
|
368 | }
|
369 |
|
370 | /**
|
371 | * Load a config file.
|
372 | * @param {string} filePath The path to a config file.
|
373 | * @param {Object} [options] The options.
|
374 | * @param {string} [options.name] The config name.
|
375 | * @param {ConfigArray} [options.parent] The parent config array.
|
376 | * @returns {ConfigArray} Loaded config.
|
377 | */
|
378 | loadFile(filePath, { name, parent } = {}) {
|
379 | const { cwd } = internalSlotsMap.get(this);
|
380 | const absolutePath = path.resolve(cwd, filePath);
|
381 |
|
382 | return createConfigArray(
|
383 | this._loadConfigData(absolutePath, name),
|
384 | parent
|
385 | );
|
386 | }
|
387 |
|
388 | /**
|
389 | * Load the config file on a given directory if exists.
|
390 | * @param {string} directoryPath The path to a directory.
|
391 | * @param {Object} [options] The options.
|
392 | * @param {string} [options.name] The config name.
|
393 | * @param {ConfigArray} [options.parent] The parent config array.
|
394 | * @returns {ConfigArray} Loaded config. An empty `ConfigArray` if any config doesn't exist.
|
395 | */
|
396 | loadInDirectory(directoryPath, { name, parent } = {}) {
|
397 | const { cwd } = internalSlotsMap.get(this);
|
398 | const absolutePath = path.resolve(cwd, directoryPath);
|
399 |
|
400 | return createConfigArray(
|
401 | this._loadConfigDataInDirectory(absolutePath, name),
|
402 | parent
|
403 | );
|
404 | }
|
405 |
|
406 | /**
|
407 | * Load a given config file.
|
408 | * @param {string} filePath The path to a config file.
|
409 | * @param {string} name The config name.
|
410 | * @returns {IterableIterator<ConfigArrayElement>} Loaded config.
|
411 | * @private
|
412 | */
|
413 | _loadConfigData(filePath, name) {
|
414 | return this._normalizeConfigData(
|
415 | loadConfigFile(filePath),
|
416 | filePath,
|
417 | name
|
418 | );
|
419 | }
|
420 |
|
421 | /**
|
422 | * Load the config file in a given directory if exists.
|
423 | * @param {string} directoryPath The path to a directory.
|
424 | * @param {string} name The config name.
|
425 | * @returns {IterableIterator<ConfigArrayElement> | null} Loaded config. `null` if any config doesn't exist.
|
426 | * @private
|
427 | */
|
428 | _loadConfigDataInDirectory(directoryPath, name) {
|
429 | for (const filename of configFilenames) {
|
430 | const filePath = path.join(directoryPath, filename);
|
431 |
|
432 | if (fs.existsSync(filePath)) {
|
433 | let configData;
|
434 |
|
435 | try {
|
436 | configData = loadConfigFile(filePath);
|
437 | } catch (error) {
|
438 | if (!error || error.code !== "ESLINT_CONFIG_FIELD_NOT_FOUND") {
|
439 | throw error;
|
440 | }
|
441 | }
|
442 |
|
443 | if (configData) {
|
444 | debug(`Config file found: ${filePath}`);
|
445 | return this._normalizeConfigData(configData, filePath, name);
|
446 | }
|
447 | }
|
448 | }
|
449 |
|
450 | debug(`Config file not found on ${directoryPath}`);
|
451 | return null;
|
452 | }
|
453 |
|
454 | /**
|
455 | * Normalize a given config to an array.
|
456 | * @param {ConfigData} configData The config data to normalize.
|
457 | * @param {string|undefined} providedFilePath The file path of this config.
|
458 | * @param {string|undefined} providedName The name of this config.
|
459 | * @returns {IterableIterator<ConfigArrayElement>} The normalized config.
|
460 | * @private
|
461 | */
|
462 | _normalizeConfigData(configData, providedFilePath, providedName) {
|
463 | const { cwd } = internalSlotsMap.get(this);
|
464 | const filePath = providedFilePath
|
465 | ? path.resolve(cwd, providedFilePath)
|
466 | : "";
|
467 | const name = providedName || (filePath && path.relative(cwd, filePath));
|
468 |
|
469 | validateConfigSchema(configData, name || filePath);
|
470 |
|
471 | return this._normalizeObjectConfigData(configData, filePath, name);
|
472 | }
|
473 |
|
474 | /**
|
475 | * Normalize a given config to an array.
|
476 | * @param {ConfigData|OverrideConfigData} configData The config data to normalize.
|
477 | * @param {string} filePath The file path of this config.
|
478 | * @param {string} name The name of this config.
|
479 | * @returns {IterableIterator<ConfigArrayElement>} The normalized config.
|
480 | * @private
|
481 | */
|
482 | *_normalizeObjectConfigData(configData, filePath, name) {
|
483 | const { cwd } = internalSlotsMap.get(this);
|
484 | const { files, excludedFiles, ...configBody } = configData;
|
485 | const basePath = filePath ? path.dirname(filePath) : cwd;
|
486 | const criteria = OverrideTester.create(files, excludedFiles, basePath);
|
487 | const elements =
|
488 | this._normalizeObjectConfigDataBody(configBody, filePath, name);
|
489 |
|
490 | // Apply the criteria to every element.
|
491 | for (const element of elements) {
|
492 |
|
493 | // Adopt the base path of the entry file (the outermost base path).
|
494 | if (element.criteria) {
|
495 | element.criteria.basePath = basePath;
|
496 | }
|
497 |
|
498 | /*
|
499 | * Merge the criteria; this is for only file extension processors in
|
500 | * `overrides` section for now.
|
501 | */
|
502 | element.criteria = OverrideTester.and(criteria, element.criteria);
|
503 |
|
504 | /*
|
505 | * Remove `root` property to ignore `root` settings which came from
|
506 | * `extends` in `overrides`.
|
507 | */
|
508 | if (element.criteria) {
|
509 | element.root = void 0;
|
510 | }
|
511 |
|
512 | yield element;
|
513 | }
|
514 | }
|
515 |
|
516 | /**
|
517 | * Normalize a given config to an array.
|
518 | * @param {ConfigData} configData The config data to normalize.
|
519 | * @param {string} filePath The file path of this config.
|
520 | * @param {string} name The name of this config.
|
521 | * @returns {IterableIterator<ConfigArrayElement>} The normalized config.
|
522 | * @private
|
523 | */
|
524 | *_normalizeObjectConfigDataBody(
|
525 | {
|
526 | env,
|
527 | extends: extend,
|
528 | globals,
|
529 | parser: parserName,
|
530 | parserOptions,
|
531 | plugins: pluginList,
|
532 | processor,
|
533 | root,
|
534 | rules,
|
535 | settings,
|
536 | overrides: overrideList = []
|
537 | },
|
538 | filePath,
|
539 | name
|
540 | ) {
|
541 | const extendList = Array.isArray(extend) ? extend : [extend];
|
542 |
|
543 | // Flatten `extends`.
|
544 | for (const extendName of extendList.filter(Boolean)) {
|
545 | yield* this._loadExtends(extendName, filePath, name);
|
546 | }
|
547 |
|
548 | // Load parser & plugins.
|
549 | const parser =
|
550 | parserName && this._loadParser(parserName, filePath, name);
|
551 | const plugins =
|
552 | pluginList && this._loadPlugins(pluginList, filePath, name);
|
553 |
|
554 | // Yield pseudo config data for file extension processors.
|
555 | if (plugins) {
|
556 | yield* this._takeFileExtensionProcessors(plugins, filePath, name);
|
557 | }
|
558 |
|
559 | // Yield the config data except `extends` and `overrides`.
|
560 | yield {
|
561 |
|
562 | // Debug information.
|
563 | name,
|
564 | filePath,
|
565 |
|
566 | // Config data.
|
567 | criteria: null,
|
568 | env,
|
569 | globals,
|
570 | parser,
|
571 | parserOptions,
|
572 | plugins,
|
573 | processor,
|
574 | root,
|
575 | rules,
|
576 | settings
|
577 | };
|
578 |
|
579 | // Flatten `overries`.
|
580 | for (let i = 0; i < overrideList.length; ++i) {
|
581 | yield* this._normalizeObjectConfigData(
|
582 | overrideList[i],
|
583 | filePath,
|
584 | `${name}#overrides[${i}]`
|
585 | );
|
586 | }
|
587 | }
|
588 |
|
589 | /**
|
590 | * Load configs of an element in `extends`.
|
591 | * @param {string} extendName The name of a base config.
|
592 | * @param {string} importerPath The file path which has the `extends` property.
|
593 | * @param {string} importerName The name of the config which has the `extends` property.
|
594 | * @returns {IterableIterator<ConfigArrayElement>} The normalized config.
|
595 | * @private
|
596 | */
|
597 | _loadExtends(extendName, importerPath, importerName) {
|
598 | debug("Loading {extends:%j} relative to %s", extendName, importerPath);
|
599 | try {
|
600 | if (extendName.startsWith("eslint:")) {
|
601 | return this._loadExtendedBuiltInConfig(
|
602 | extendName,
|
603 | importerName
|
604 | );
|
605 | }
|
606 | if (extendName.startsWith("plugin:")) {
|
607 | return this._loadExtendedPluginConfig(
|
608 | extendName,
|
609 | importerPath,
|
610 | importerName
|
611 | );
|
612 | }
|
613 | return this._loadExtendedShareableConfig(
|
614 | extendName,
|
615 | importerPath,
|
616 | importerName
|
617 | );
|
618 | } catch (error) {
|
619 | error.message += `\nReferenced from: ${importerPath || importerName}`;
|
620 | throw error;
|
621 | }
|
622 | }
|
623 |
|
624 | /**
|
625 | * Load configs of an element in `extends`.
|
626 | * @param {string} extendName The name of a base config.
|
627 | * @param {string} importerName The name of the config which has the `extends` property.
|
628 | * @returns {IterableIterator<ConfigArrayElement>} The normalized config.
|
629 | * @private
|
630 | */
|
631 | _loadExtendedBuiltInConfig(extendName, importerName) {
|
632 | const name = `${importerName} » ${extendName}`;
|
633 |
|
634 | if (extendName === "eslint:recommended") {
|
635 | return this._loadConfigData(eslintRecommendedPath, name);
|
636 | }
|
637 | if (extendName === "eslint:all") {
|
638 | return this._loadConfigData(eslintAllPath, name);
|
639 | }
|
640 |
|
641 | throw configMissingError(extendName, importerName);
|
642 | }
|
643 |
|
644 | /**
|
645 | * Load configs of an element in `extends`.
|
646 | * @param {string} extendName The name of a base config.
|
647 | * @param {string} importerPath The file path which has the `extends` property.
|
648 | * @param {string} importerName The name of the config which has the `extends` property.
|
649 | * @returns {IterableIterator<ConfigArrayElement>} The normalized config.
|
650 | * @private
|
651 | */
|
652 | _loadExtendedPluginConfig(extendName, importerPath, importerName) {
|
653 | const slashIndex = extendName.lastIndexOf("/");
|
654 | const pluginName = extendName.slice("plugin:".length, slashIndex);
|
655 | const configName = extendName.slice(slashIndex + 1);
|
656 |
|
657 | if (isFilePath(pluginName)) {
|
658 | throw new Error("'extends' cannot use a file path for plugins.");
|
659 | }
|
660 |
|
661 | const plugin = this._loadPlugin(pluginName, importerPath, importerName);
|
662 | const configData =
|
663 | plugin.definition &&
|
664 | plugin.definition.configs[configName];
|
665 |
|
666 | if (configData) {
|
667 | return this._normalizeConfigData(
|
668 | configData,
|
669 | plugin.filePath,
|
670 | `${importerName} » plugin:${plugin.id}/${configName}`
|
671 | );
|
672 | }
|
673 |
|
674 | throw plugin.error || configMissingError(extendName, importerPath);
|
675 | }
|
676 |
|
677 | /**
|
678 | * Load configs of an element in `extends`.
|
679 | * @param {string} extendName The name of a base config.
|
680 | * @param {string} importerPath The file path which has the `extends` property.
|
681 | * @param {string} importerName The name of the config which has the `extends` property.
|
682 | * @returns {IterableIterator<ConfigArrayElement>} The normalized config.
|
683 | * @private
|
684 | */
|
685 | _loadExtendedShareableConfig(extendName, importerPath, importerName) {
|
686 | const { cwd } = internalSlotsMap.get(this);
|
687 | const relativeTo = importerPath || path.join(cwd, "__placeholder__.js");
|
688 | let request;
|
689 |
|
690 | if (isFilePath(extendName)) {
|
691 | request = extendName;
|
692 | } else if (extendName.startsWith(".")) {
|
693 | request = `./${extendName}`; // For backward compatibility. A ton of tests depended on this behavior.
|
694 | } else {
|
695 | request = naming.normalizePackageName(
|
696 | extendName,
|
697 | "eslint-config"
|
698 | );
|
699 | }
|
700 |
|
701 | let filePath;
|
702 |
|
703 | try {
|
704 | filePath = ModuleResolver.resolve(request, relativeTo);
|
705 | } catch (error) {
|
706 | /* istanbul ignore else */
|
707 | if (error && error.code === "MODULE_NOT_FOUND") {
|
708 | throw configMissingError(extendName, importerPath);
|
709 | }
|
710 | throw error;
|
711 | }
|
712 |
|
713 | writeDebugLogForLoading(request, relativeTo, filePath);
|
714 | return this._loadConfigData(filePath, `${importerName} » ${request}`);
|
715 | }
|
716 |
|
717 | /**
|
718 | * Load given plugins.
|
719 | * @param {string[]} names The plugin names to load.
|
720 | * @param {string} importerPath The path to a config file that imports it. This is just a debug info.
|
721 | * @param {string} importerName The name of a config file that imports it. This is just a debug info.
|
722 | * @returns {Record<string,DependentPlugin>} The loaded parser.
|
723 | * @private
|
724 | */
|
725 | _loadPlugins(names, importerPath, importerName) {
|
726 | return names.reduce((map, name) => {
|
727 | if (isFilePath(name)) {
|
728 | throw new Error("Plugins array cannot includes file paths.");
|
729 | }
|
730 | const plugin = this._loadPlugin(name, importerPath, importerName);
|
731 |
|
732 | map[plugin.id] = plugin;
|
733 |
|
734 | return map;
|
735 | }, {});
|
736 | }
|
737 |
|
738 | /**
|
739 | * Load a given parser.
|
740 | * @param {string} nameOrPath The package name or the path to a parser file.
|
741 | * @param {string} importerPath The path to a config file that imports it.
|
742 | * @param {string} importerName The name of a config file that imports it. This is just a debug info.
|
743 | * @returns {DependentParser} The loaded parser.
|
744 | */
|
745 | _loadParser(nameOrPath, importerPath, importerName) {
|
746 | debug("Loading parser %j from %s", nameOrPath, importerPath);
|
747 |
|
748 | const { cwd } = internalSlotsMap.get(this);
|
749 | const relativeTo = importerPath || path.join(cwd, "__placeholder__.js");
|
750 |
|
751 | try {
|
752 | const filePath = ModuleResolver.resolve(nameOrPath, relativeTo);
|
753 |
|
754 | writeDebugLogForLoading(nameOrPath, relativeTo, filePath);
|
755 |
|
756 | return new ConfigDependency({
|
757 | definition: require(filePath),
|
758 | filePath,
|
759 | id: nameOrPath,
|
760 | importerName,
|
761 | importerPath
|
762 | });
|
763 | } catch (error) {
|
764 |
|
765 | // If the parser name is "espree", load the espree of ESLint.
|
766 | if (nameOrPath === "espree") {
|
767 | debug("Fallback espree.");
|
768 | return new ConfigDependency({
|
769 | definition: require("espree"),
|
770 | filePath: require.resolve("espree"),
|
771 | id: nameOrPath,
|
772 | importerName,
|
773 | importerPath
|
774 | });
|
775 | }
|
776 |
|
777 | debug("Failed to load parser '%s' declared in '%s'.", nameOrPath, importerName);
|
778 | error.message = `Failed to load parser '${nameOrPath}' declared in '${importerName}': ${error.message}`;
|
779 |
|
780 | return new ConfigDependency({
|
781 | error,
|
782 | id: nameOrPath,
|
783 | importerName,
|
784 | importerPath
|
785 | });
|
786 | }
|
787 | }
|
788 |
|
789 | /**
|
790 | * Load a given plugin.
|
791 | * @param {string} name The plugin name to load.
|
792 | * @param {string} importerPath The path to a config file that imports it. This is just a debug info.
|
793 | * @param {string} importerName The name of a config file that imports it. This is just a debug info.
|
794 | * @returns {DependentPlugin} The loaded plugin.
|
795 | * @private
|
796 | */
|
797 | _loadPlugin(name, importerPath, importerName) {
|
798 | debug("Loading plugin %j from %s", name, importerPath);
|
799 |
|
800 | const { additionalPluginPool, resolvePluginsRelativeTo } = internalSlotsMap.get(this);
|
801 | const request = naming.normalizePackageName(name, "eslint-plugin");
|
802 | const id = naming.getShorthandName(request, "eslint-plugin");
|
803 | const relativeTo = path.join(resolvePluginsRelativeTo, "__placeholder__.js");
|
804 |
|
805 | if (name.match(/\s+/u)) {
|
806 | const error = Object.assign(
|
807 | new Error(`Whitespace found in plugin name '${name}'`),
|
808 | {
|
809 | messageTemplate: "whitespace-found",
|
810 | messageData: { pluginName: request }
|
811 | }
|
812 | );
|
813 |
|
814 | return new ConfigDependency({
|
815 | error,
|
816 | id,
|
817 | importerName,
|
818 | importerPath
|
819 | });
|
820 | }
|
821 |
|
822 | // Check for additional pool.
|
823 | const plugin =
|
824 | additionalPluginPool.get(request) ||
|
825 | additionalPluginPool.get(id);
|
826 |
|
827 | if (plugin) {
|
828 | return new ConfigDependency({
|
829 | definition: normalizePlugin(plugin),
|
830 | filePath: importerPath,
|
831 | id,
|
832 | importerName,
|
833 | importerPath
|
834 | });
|
835 | }
|
836 |
|
837 | let filePath;
|
838 | let error;
|
839 |
|
840 | try {
|
841 | filePath = ModuleResolver.resolve(request, relativeTo);
|
842 | } catch (resolveError) {
|
843 | error = resolveError;
|
844 | /* istanbul ignore else */
|
845 | if (error && error.code === "MODULE_NOT_FOUND") {
|
846 | error.messageTemplate = "plugin-missing";
|
847 | error.messageData = {
|
848 | pluginName: request,
|
849 | resolvePluginsRelativeTo,
|
850 | importerName
|
851 | };
|
852 | }
|
853 | }
|
854 |
|
855 | if (filePath) {
|
856 | try {
|
857 | writeDebugLogForLoading(request, relativeTo, filePath);
|
858 | return new ConfigDependency({
|
859 | definition: normalizePlugin(require(filePath)),
|
860 | filePath,
|
861 | id,
|
862 | importerName,
|
863 | importerPath
|
864 | });
|
865 | } catch (loadError) {
|
866 | error = loadError;
|
867 | }
|
868 | }
|
869 |
|
870 | debug("Failed to load plugin '%s' declared in '%s'.", name, importerName);
|
871 | error.message = `Failed to load plugin '${name}' declared in '${importerName}': ${error.message}`;
|
872 | return new ConfigDependency({
|
873 | error,
|
874 | id,
|
875 | importerName,
|
876 | importerPath
|
877 | });
|
878 | }
|
879 |
|
880 | /**
|
881 | * Take file expression processors as config array elements.
|
882 | * @param {Record<string,DependentPlugin>} plugins The plugin definitions.
|
883 | * @param {string} filePath The file path of this config.
|
884 | * @param {string} name The name of this config.
|
885 | * @returns {IterableIterator<ConfigArrayElement>} The config array elements of file expression processors.
|
886 | * @private
|
887 | */
|
888 | *_takeFileExtensionProcessors(plugins, filePath, name) {
|
889 | for (const pluginId of Object.keys(plugins)) {
|
890 | const processors =
|
891 | plugins[pluginId] &&
|
892 | plugins[pluginId].definition &&
|
893 | plugins[pluginId].definition.processors;
|
894 |
|
895 | if (!processors) {
|
896 | continue;
|
897 | }
|
898 |
|
899 | for (const processorId of Object.keys(processors)) {
|
900 | if (processorId.startsWith(".")) {
|
901 | yield* this._normalizeObjectConfigData(
|
902 | {
|
903 | files: [`*${processorId}`],
|
904 | processor: `${pluginId}/${processorId}`
|
905 | },
|
906 | filePath,
|
907 | `${name}#processors["${pluginId}/${processorId}"]`
|
908 | );
|
909 | }
|
910 | }
|
911 | }
|
912 | }
|
913 | }
|
914 |
|
915 | module.exports = { ConfigArrayFactory };
|