plugins.js 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169
  1. /**
  2. * @fileoverview Plugins manager
  3. * @author Nicholas C. Zakas
  4. */
  5. "use strict";
  6. //------------------------------------------------------------------------------
  7. // Requirements
  8. //------------------------------------------------------------------------------
  9. const debug = require("debug")("eslint:plugins");
  10. const naming = require("../util/naming");
  11. const path = require("path");
  12. //------------------------------------------------------------------------------
  13. // Public Interface
  14. //------------------------------------------------------------------------------
  15. /**
  16. * Plugin class
  17. */
  18. class Plugins {
  19. /**
  20. * Creates the plugins context
  21. * @param {Environments} envContext - env context
  22. * @param {function(string, Rule): void} defineRule - Callback for when a plugin is defined which introduces rules
  23. */
  24. constructor(envContext, defineRule) {
  25. this._plugins = Object.create(null);
  26. this._environments = envContext;
  27. this._defineRule = defineRule;
  28. }
  29. /**
  30. * Defines a plugin with a given name rather than loading from disk.
  31. * @param {string} pluginName The name of the plugin to load.
  32. * @param {Object} plugin The plugin object.
  33. * @returns {void}
  34. */
  35. define(pluginName, plugin) {
  36. const longName = naming.normalizePackageName(pluginName, "eslint-plugin");
  37. const shortName = naming.getShorthandName(longName, "eslint-plugin");
  38. // load up environments and rules
  39. this._plugins[shortName] = plugin;
  40. this._environments.importPlugin(plugin, shortName);
  41. if (plugin.rules) {
  42. Object.keys(plugin.rules).forEach(ruleId => {
  43. const qualifiedRuleId = `${shortName}/${ruleId}`,
  44. rule = plugin.rules[ruleId];
  45. this._defineRule(qualifiedRuleId, rule);
  46. });
  47. }
  48. }
  49. /**
  50. * Gets a plugin with the given name.
  51. * @param {string} pluginName The name of the plugin to retrieve.
  52. * @returns {Object} The plugin or null if not loaded.
  53. */
  54. get(pluginName) {
  55. return this._plugins[pluginName] || null;
  56. }
  57. /**
  58. * Returns all plugins that are loaded.
  59. * @returns {Object} The plugins cache.
  60. */
  61. getAll() {
  62. return this._plugins;
  63. }
  64. /**
  65. * Loads a plugin with the given name.
  66. * @param {string} pluginName The name of the plugin to load.
  67. * @returns {void}
  68. * @throws {Error} If the plugin cannot be loaded.
  69. */
  70. load(pluginName) {
  71. const longName = naming.normalizePackageName(pluginName, "eslint-plugin");
  72. const shortName = naming.getShorthandName(longName, "eslint-plugin");
  73. let plugin = null;
  74. if (pluginName.match(/\s+/u)) {
  75. const whitespaceError = new Error(`Whitespace found in plugin name '${pluginName}'`);
  76. whitespaceError.messageTemplate = "whitespace-found";
  77. whitespaceError.messageData = {
  78. pluginName: longName
  79. };
  80. throw whitespaceError;
  81. }
  82. if (!this._plugins[shortName]) {
  83. try {
  84. plugin = require(longName);
  85. } catch (pluginLoadErr) {
  86. try {
  87. // Check whether the plugin exists
  88. require.resolve(longName);
  89. } catch (missingPluginErr) {
  90. // If the plugin can't be resolved, display the missing plugin error (usually a config or install error)
  91. debug(`Failed to load plugin ${longName}.`);
  92. missingPluginErr.message = `Failed to load plugin ${pluginName}: ${missingPluginErr.message}`;
  93. missingPluginErr.messageTemplate = "plugin-missing";
  94. missingPluginErr.messageData = {
  95. pluginName: longName,
  96. eslintPath: path.resolve(__dirname, "../..")
  97. };
  98. throw missingPluginErr;
  99. }
  100. // Otherwise, the plugin exists and is throwing on module load for some reason, so print the stack trace.
  101. throw pluginLoadErr;
  102. }
  103. // This step is costly, so skip if debug is disabled
  104. if (debug.enabled) {
  105. const resolvedPath = require.resolve(longName);
  106. let version = null;
  107. try {
  108. version = require(`${longName}/package.json`).version;
  109. } catch (e) {
  110. // Do nothing
  111. }
  112. const loadedPluginAndVersion = version
  113. ? `${longName}@${version}`
  114. : `${longName}, version unknown`;
  115. debug(`Loaded plugin ${pluginName} (${loadedPluginAndVersion}) (from ${resolvedPath})`);
  116. }
  117. this.define(pluginName, plugin);
  118. }
  119. }
  120. /**
  121. * Loads all plugins from an array.
  122. * @param {string[]} pluginNames An array of plugins names.
  123. * @returns {void}
  124. * @throws {Error} If a plugin cannot be loaded.
  125. * @throws {Error} If "plugins" in config is not an array
  126. */
  127. loadAll(pluginNames) {
  128. // if "plugins" in config is not an array, throw an error so user can fix their config.
  129. if (!Array.isArray(pluginNames)) {
  130. const pluginNotArrayMessage = "ESLint configuration error: \"plugins\" value must be an array";
  131. debug(`${pluginNotArrayMessage}: ${JSON.stringify(pluginNames)}`);
  132. throw new Error(pluginNotArrayMessage);
  133. }
  134. // load each plugin by name
  135. pluginNames.forEach(this.load, this);
  136. }
  137. }
  138. module.exports = Plugins;