UNPKG

6.27 kBJavaScriptView Raw
1/**
2 * @fileoverview Plugins manager
3 * @author Nicholas C. Zakas
4 */
5"use strict";
6
7//------------------------------------------------------------------------------
8// Requirements
9//------------------------------------------------------------------------------
10
11const debug = require("debug")("eslint:plugins");
12
13//------------------------------------------------------------------------------
14// Private
15//------------------------------------------------------------------------------
16
17const PLUGIN_NAME_PREFIX = "eslint-plugin-",
18 NAMESPACE_REGEX = /^@.*\//i;
19
20//------------------------------------------------------------------------------
21// Public Interface
22//------------------------------------------------------------------------------
23
24/**
25 * Plugin class
26 */
27class Plugins {
28
29 /**
30 * Creates the plugins context
31 * @param {Environments} envContext - env context
32 * @param {Rules} rulesContext - rules context
33 */
34 constructor(envContext, rulesContext) {
35 this._plugins = Object.create(null);
36 this._environments = envContext;
37 this._rules = rulesContext;
38 }
39
40 /**
41 * Removes the prefix `eslint-plugin-` from a plugin name.
42 * @param {string} pluginName The name of the plugin which may have the prefix.
43 * @returns {string} The name of the plugin without prefix.
44 */
45 static removePrefix(pluginName) {
46 return pluginName.startsWith(PLUGIN_NAME_PREFIX) ? pluginName.slice(PLUGIN_NAME_PREFIX.length) : pluginName;
47 }
48
49 /**
50 * Gets the scope (namespace) of a plugin.
51 * @param {string} pluginName The name of the plugin which may have the prefix.
52 * @returns {string} The name of the plugins namepace if it has one.
53 */
54 static getNamespace(pluginName) {
55 return pluginName.match(NAMESPACE_REGEX) ? pluginName.match(NAMESPACE_REGEX)[0] : "";
56 }
57
58 /**
59 * Removes the namespace from a plugin name.
60 * @param {string} pluginName The name of the plugin which may have the prefix.
61 * @returns {string} The name of the plugin without the namespace.
62 */
63 static removeNamespace(pluginName) {
64 return pluginName.replace(NAMESPACE_REGEX, "");
65 }
66
67 /**
68 * Defines a plugin with a given name rather than loading from disk.
69 * @param {string} pluginName The name of the plugin to load.
70 * @param {Object} plugin The plugin object.
71 * @returns {void}
72 */
73 define(pluginName, plugin) {
74 const pluginNamespace = Plugins.getNamespace(pluginName),
75 pluginNameWithoutNamespace = Plugins.removeNamespace(pluginName),
76 pluginNameWithoutPrefix = Plugins.removePrefix(pluginNameWithoutNamespace),
77 shortName = pluginNamespace + pluginNameWithoutPrefix;
78
79 // load up environments and rules
80 this._plugins[shortName] = plugin;
81 this._environments.importPlugin(plugin, shortName);
82 this._rules.importPlugin(plugin, shortName);
83 }
84
85 /**
86 * Gets a plugin with the given name.
87 * @param {string} pluginName The name of the plugin to retrieve.
88 * @returns {Object} The plugin or null if not loaded.
89 */
90 get(pluginName) {
91 return this._plugins[pluginName] || null;
92 }
93
94 /**
95 * Returns all plugins that are loaded.
96 * @returns {Object} The plugins cache.
97 */
98 getAll() {
99 return this._plugins;
100 }
101
102 /**
103 * Loads a plugin with the given name.
104 * @param {string} pluginName The name of the plugin to load.
105 * @returns {void}
106 * @throws {Error} If the plugin cannot be loaded.
107 */
108 load(pluginName) {
109 const pluginNamespace = Plugins.getNamespace(pluginName),
110 pluginNameWithoutNamespace = Plugins.removeNamespace(pluginName),
111 pluginNameWithoutPrefix = Plugins.removePrefix(pluginNameWithoutNamespace),
112 shortName = pluginNamespace + pluginNameWithoutPrefix,
113 longName = pluginNamespace + PLUGIN_NAME_PREFIX + pluginNameWithoutPrefix;
114 let plugin = null;
115
116 if (pluginName.match(/\s+/)) {
117 const whitespaceError = new Error(`Whitespace found in plugin name '${pluginName}'`);
118
119 whitespaceError.messageTemplate = "whitespace-found";
120 whitespaceError.messageData = {
121 pluginName: longName
122 };
123 throw whitespaceError;
124 }
125
126 if (!this._plugins[shortName]) {
127 try {
128 plugin = require(longName);
129 } catch (pluginLoadErr) {
130 try {
131
132 // Check whether the plugin exists
133 require.resolve(longName);
134 } catch (missingPluginErr) {
135
136 // If the plugin can't be resolved, display the missing plugin error (usually a config or install error)
137 debug(`Failed to load plugin ${longName}.`);
138 missingPluginErr.message = `Failed to load plugin ${pluginName}: ${missingPluginErr.message}`;
139 missingPluginErr.messageTemplate = "plugin-missing";
140 missingPluginErr.messageData = {
141 pluginName: longName
142 };
143 throw missingPluginErr;
144 }
145
146 // Otherwise, the plugin exists and is throwing on module load for some reason, so print the stack trace.
147 throw pluginLoadErr;
148 }
149
150 this.define(pluginName, plugin);
151 }
152 }
153
154 /**
155 * Loads all plugins from an array.
156 * @param {string[]} pluginNames An array of plugins names.
157 * @returns {void}
158 * @throws {Error} If a plugin cannot be loaded.
159 * @throws {Error} If "plugins" in config is not an array
160 */
161 loadAll(pluginNames) {
162
163 // if "plugins" in config is not an array, throw an error so user can fix their config.
164 if (!Array.isArray(pluginNames)) {
165 const pluginNotArrayMessage = "ESLint configuration error: \"plugins\" value must be an array";
166
167 debug(`${pluginNotArrayMessage}: ${JSON.stringify(pluginNames)}`);
168
169 throw new Error(pluginNotArrayMessage);
170 }
171
172 // load each plugin by name
173 pluginNames.forEach(this.load, this);
174 }
175}
176
177module.exports = Plugins;