plugins.js 5.8 KB

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