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