Skip to content

Commit

Permalink
Ensure plugin-missing error message is used when extending plugin con…
Browse files Browse the repository at this point in the history
…figs
  • Loading branch information
not-an-aardvark committed Feb 20, 2019
1 parent 8842a3b commit ecfb5f5
Show file tree
Hide file tree
Showing 5 changed files with 70 additions and 31 deletions.
6 changes: 5 additions & 1 deletion lib/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,11 @@ class Config {
this._rootConfigReferencePath = path.join(this.options.cwd, "__placeholder__.js");

this.baseConfig = options.baseConfig
? ConfigFile.loadObject(this, { config: options.baseConfig, filePath: this._rootConfigReferencePath, configFullName: "(Provided base config)" })
? ConfigFile.loadObject(this, {
config: options.baseConfig,
filePath: path.join(this.options.cwd, "__baseConfig_placeholder_filename__.js"),
configFullName: "(Provided base config)"
})
: { rules: {} };
this.configCache.setConfig(BASE_CONFIG_FILEPATH_CACHE_KEY, this.baseConfig);

Expand Down
17 changes: 11 additions & 6 deletions lib/config/config-file.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ const fs = require("fs"),
naming = require("../util/naming"),
stripComments = require("strip-json-comments"),
stringify = require("json-stable-stringify-without-jsonify"),
importFresh = require("import-fresh");
importFresh = require("import-fresh"),
Plugins = require("./plugins");

const debug = require("debug")("eslint:config-file");

Expand Down Expand Up @@ -386,16 +387,21 @@ function applyExtends(config, configContext, filePath) {

// eslint-disable-next-line no-use-before-define
return ConfigOps.merge(load(extendedConfigReference, configContext, filePath), previousValue);
} catch (e) {
} catch (err) {

/*
* If the file referenced by `extends` failed to load, add the path
* to the configuration file that referenced it to the error
* message so the user is able to see where it was referenced from,
* then re-throw.
*/
e.message += `\nReferenced from: ${filePath}`;
throw e;
err.message += `\nReferenced from: ${filePath}`;

if (err.messageTemplate === "plugin-missing") {
err.messageData.configStack.push(filePath);
}

throw err;
}

}, Object.assign({}, config));
Expand Down Expand Up @@ -429,10 +435,9 @@ function resolve(extendedConfigReference, referencedFromPath, pluginRootPath) {
const configFullName = extendedConfigReference;
const pluginName = extendedConfigReference.slice(7, extendedConfigReference.lastIndexOf("/"));
const configName = extendedConfigReference.slice(extendedConfigReference.lastIndexOf("/") + 1);
const normalizedPluginName = naming.normalizePackageName(pluginName, "eslint-plugin");

return {
filePath: relativeModuleResolver(normalizedPluginName, path.join(pluginRootPath, "__placeholder__.js")),
filePath: Plugins.resolve(pluginName, pluginRootPath),
configName,
configFullName
};
Expand Down
58 changes: 34 additions & 24 deletions lib/config/plugins.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,45 +76,55 @@ class Plugins {
return this._plugins;
}

/**
* Resolves a plugin with the given name
* @param {string} pluginName The name of the plugin to resolve
* @param {string} pluginRootPath The absolute path to the directory where the plugin should be resolved from
* @returns {string} The full path to the plugin module
* @throws {Error} An templated error with debugging information if the plugin cannot be loaded.
*/
static resolve(pluginName, pluginRootPath) {
const longName = naming.normalizePackageName(pluginName, "eslint-plugin");
const pathToResolveRelativeTo = path.join(pluginRootPath, "__placeholder__.js");

try {
return relativeModuleResolver(longName, pathToResolveRelativeTo);
} catch (missingPluginErr) {

// If the plugin can't be resolved, display the missing plugin error (usually a config or install error)
debug(`Failed to load plugin ${longName} from ${pluginRootPath}.`);
missingPluginErr.message = `Failed to load plugin ${pluginName} from ${pluginRootPath}: ${missingPluginErr.message}`;
missingPluginErr.messageTemplate = "plugin-missing";
missingPluginErr.messageData = {
pluginName: longName,
pluginRootPath,
configStack: []
};

throw missingPluginErr;
}
}

/**
* Loads a plugin with the given name.
* @param {string} pluginName The name of the plugin to load.
* @returns {void}
* @throws {Error} If the plugin cannot be loaded.
*/
load(pluginName) {
const longName = naming.normalizePackageName(pluginName, "eslint-plugin");
const shortName = naming.getShorthandName(longName, "eslint-plugin");

if (pluginName.match(/\s+/)) {
const whitespaceError = new Error(`Whitespace found in plugin name '${pluginName}'`);

whitespaceError.messageTemplate = "whitespace-found";
whitespaceError.messageData = {
pluginName: longName
};
whitespaceError.messageData = { pluginName };
throw whitespaceError;
}

if (!this._plugins[shortName]) {
const resolveRelativeToPath = path.join(this._pluginRootPath, "__placeholder__.js");
let pluginPath;

try {
pluginPath = relativeModuleResolver(longName, resolveRelativeToPath);
} catch (missingPluginErr) {

// If the plugin can't be resolved, display the missing plugin error (usually a config or install error)
debug(`Failed to load plugin ${longName} from ${this._pluginRootPath}.`);
missingPluginErr.message = `Failed to load plugin ${pluginName} from ${this._pluginRootPath}: ${missingPluginErr.message}`;
missingPluginErr.messageTemplate = "plugin-missing";
missingPluginErr.messageData = {
pluginName: longName,
pluginRootPath: this._pluginRootPath
};
throw missingPluginErr;
}
const longName = naming.normalizePackageName(pluginName, "eslint-plugin");
const shortName = naming.getShorthandName(longName, "eslint-plugin");

if (!this._plugins[shortName]) {
const pluginPath = Plugins.resolve(shortName, this._pluginRootPath);
const plugin = require(pluginPath);

// This step is costly, so skip if debug is disabled
Expand Down
2 changes: 2 additions & 0 deletions messages/plugin-missing.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,6 @@ It's likely that the plugin isn't installed correctly. Try reinstalling by runni

npm install <%- pluginName %>@latest --save-dev

The plugin "<%- pluginName %>" was referenced from the config file in <%- configStack.join("\n\t...which was loaded by the config file in ") %>.

If you still can't figure out the problem, please stop by https://gitter.im/eslint/eslint to chat with the team.
18 changes: 18 additions & 0 deletions tests/lib/config/config-file.js
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,25 @@ describe("ConfigFile", () => {
rules: { eqeqeq: 2 }
}, configContext, "/whatever");
}, /Failed to load config "plugin:enable-nonexistent-parser\/baz" to extend from./);
});

it("should throw an error with a message template when a plugin is not found", () => {
try {
ConfigFile.applyExtends({
extends: "plugin:nonexistent-plugin/baz",
rules: { eqeqeq: 2 }
}, configContext, "/whatever");
} catch (err) {
assert.strictEqual(err.messageTemplate, "plugin-missing");
assert.deepStrictEqual(err.messageData, {
pluginName: "eslint-plugin-nonexistent-plugin",
pluginRootPath: getFixturePath("."),
configStack: ["/whatever"]
});

return;
}
assert.fail("Expected to throw an error");
});

it("should apply extensions recursively when specified from package", () => {
Expand Down

0 comments on commit ecfb5f5

Please sign in to comment.