Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Breaking: improve plugin resolving (refs eslint/rfcs#47) #12922

Merged
merged 15 commits into from Mar 27, 2020
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs/user-guide/configuring.md
Expand Up @@ -321,7 +321,8 @@ And in YAML:
- eslint-plugin-plugin2
```

**Note:** Plugins are resolved relative to the current working directory of the ESLint process. In other words, ESLint will load the same plugin as a user would obtain by running `require('eslint-plugin-pluginname')` in a Node REPL from their project root.
* **Note1:** Plugins are resolved relative to the config file. In other words, ESLint will load the plugin as a user would obtain by running `require('eslint-plugin-pluginname')` in the config file.
mysticatea marked this conversation as resolved.
Show resolved Hide resolved
* **Note2:** Plugins in the base configuration (loaded by `extends` setting) are relative to the derived config file. For example, if `./.eslintrc` has `extends: ["foo"]` and the `eslint-config-foo` has `plugins: ["bar"]`, ESLint finds the `eslint-plugin-bar` from `./node_modules/` (rather than `./node_modules/eslint-config-foo/node_modules/`) or ancestor directories. Thus every plugin in the config file and base configurations is resolved uniquely.

### Naming Convention

Expand Down
2 changes: 1 addition & 1 deletion lib/cli-engine/cascading-config-array-factory.js
Expand Up @@ -199,7 +199,7 @@ class CascadingConfigArrayFactory {
cliConfig: cliConfigData = null,
cwd = process.cwd(),
ignorePath,
resolvePluginsRelativeTo = cwd,
resolvePluginsRelativeTo,
rulePaths = [],
specificConfigPath = null,
useEslintrc = true
Expand Down
68 changes: 41 additions & 27 deletions lib/cli-engine/config-array-factory.js
Expand Up @@ -90,7 +90,16 @@ const configFilenames = [
* @typedef {Object} ConfigArrayFactoryInternalSlots
* @property {Map<string,Plugin>} additionalPluginPool The map for additional plugins.
* @property {string} cwd The path to the current working directory.
* @property {string} resolvePluginsRelativeTo An absolute path the the directory that plugins should be resolved from.
* @property {string | undefined} resolvePluginsRelativeTo An absolute path the the directory that plugins should be resolved from.
*/

/**
* @typedef {Object} ConfigArrayFactoryLoadingContext
* @property {string} filePath The path to the current configuration.
* @property {string} matchBasePath The base path to resolve relative paths in `overrides[].files`, `overrides[].excludedFiles`, and `ignorePatterns`.
* @property {string} name The name of the current configuration.
* @property {string} pluginBasePath The base path to resolve plugins.
* @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.
*/

/**
Expand Down Expand Up @@ -337,15 +346,15 @@ function writeDebugLogForLoading(request, relativeTo, filePath) {

/**
* Create a new context with default values.
* @param {string | undefined} cwd The current working directory.
* @param {ConfigArrayFactoryInternalSlots} slots The internal slots.
* @param {"config" | "ignore" | "implicit-processor" | undefined} providedType The type of the current configuration. Default is `"config"`.
* @param {string | undefined} providedName The name of the current configuration. Default is the relative path from `cwd` to `filePath`.
* @param {string | undefined} providedFilePath The path to the current configuration. Default is empty string.
* @param {string | undefined} providedMatchBasePath The type of the current configuration. Default is the directory of `filePath` or `cwd`.
* @returns {ConfigArrayFactoryLoadingContext} The created context.
*/
function createContext(
cwd,
{ cwd, resolvePluginsRelativeTo },
providedType,
providedName,
providedFilePath,
Expand All @@ -355,16 +364,20 @@ function createContext(
? path.resolve(cwd, providedFilePath)
: "";
const matchBasePath =
providedMatchBasePath ||
(providedMatchBasePath && path.resolve(cwd, providedMatchBasePath)) ||
(filePath && path.dirname(filePath)) ||
cwd;
const name =
providedName ||
(filePath && path.relative(cwd, filePath)) ||
"";
const pluginBasePath =
resolvePluginsRelativeTo ||
(filePath && path.dirname(filePath)) ||
cwd;
const type = providedType || "config";

return { filePath, matchBasePath, name, type };
return { filePath, matchBasePath, name, pluginBasePath, type };
}

/**
Expand Down Expand Up @@ -399,12 +412,14 @@ class ConfigArrayFactory {
constructor({
additionalPluginPool = new Map(),
cwd = process.cwd(),
resolvePluginsRelativeTo = cwd
resolvePluginsRelativeTo
} = {}) {
internalSlotsMap.set(this, {
additionalPluginPool,
cwd,
resolvePluginsRelativeTo: path.resolve(cwd, resolvePluginsRelativeTo)
resolvePluginsRelativeTo:
resolvePluginsRelativeTo &&
path.resolve(cwd, resolvePluginsRelativeTo)
});
}

Expand All @@ -422,8 +437,8 @@ class ConfigArrayFactory {
return new ConfigArray();
}

const { cwd } = internalSlotsMap.get(this);
const ctx = createContext(cwd, "config", name, filePath, basePath);
const slots = internalSlotsMap.get(this);
const ctx = createContext(slots, "config", name, filePath, basePath);
const elements = this._normalizeConfigData(configData, ctx);

return new ConfigArray(...elements);
Expand All @@ -438,8 +453,8 @@ class ConfigArrayFactory {
* @returns {ConfigArray} Loaded config.
*/
loadFile(filePath, { basePath, name } = {}) {
const { cwd } = internalSlotsMap.get(this);
const ctx = createContext(cwd, "config", name, filePath, basePath);
const slots = internalSlotsMap.get(this);
const ctx = createContext(slots, "config", name, filePath, basePath);

return new ConfigArray(...this._loadConfigData(ctx));
}
Expand All @@ -453,11 +468,11 @@ class ConfigArrayFactory {
* @returns {ConfigArray} Loaded config. An empty `ConfigArray` if any config doesn't exist.
*/
loadInDirectory(directoryPath, { basePath, name } = {}) {
const { cwd } = internalSlotsMap.get(this);
const slots = internalSlotsMap.get(this);

for (const filename of configFilenames) {
const ctx = createContext(
cwd,
slots,
"config",
name,
path.join(directoryPath, filename),
Expand Down Expand Up @@ -517,16 +532,15 @@ class ConfigArrayFactory {
* @returns {ConfigArray} Loaded config. An empty `ConfigArray` if any config doesn't exist.
*/
loadESLintIgnore(filePath) {
const { cwd } = internalSlotsMap.get(this);
const absolutePath = path.resolve(cwd, filePath);
const ignorePatterns = loadESLintIgnoreFile(absolutePath);
const slots = internalSlotsMap.get(this);
const ctx = createContext(
cwd,
slots,
"ignore",
void 0,
absolutePath,
cwd
filePath,
slots.cwd
);
const ignorePatterns = loadESLintIgnoreFile(ctx.filePath);

return new ConfigArray(
...this._normalizeESLintIgnoreData(ignorePatterns, ctx)
Expand All @@ -538,9 +552,9 @@ class ConfigArrayFactory {
* @returns {ConfigArray} Loaded config. An empty `ConfigArray` if any config doesn't exist.
*/
loadDefaultESLintIgnore() {
const { cwd } = internalSlotsMap.get(this);
const eslintIgnorePath = path.resolve(cwd, ".eslintignore");
const packageJsonPath = path.resolve(cwd, "package.json");
const slots = internalSlotsMap.get(this);
const eslintIgnorePath = path.resolve(slots.cwd, ".eslintignore");
const packageJsonPath = path.resolve(slots.cwd, "package.json");

if (fs.existsSync(eslintIgnorePath)) {
return this.loadESLintIgnore(eslintIgnorePath);
Expand All @@ -553,11 +567,11 @@ class ConfigArrayFactory {
throw new Error("Package.json eslintIgnore property requires an array of paths");
}
const ctx = createContext(
cwd,
slots,
"ignore",
"eslintIgnore in package.json",
packageJsonPath,
cwd
slots.cwd
);

return new ConfigArray(
Expand Down Expand Up @@ -934,10 +948,10 @@ class ConfigArrayFactory {
_loadPlugin(name, ctx) {
debug("Loading plugin %j from %s", name, ctx.filePath);

const { additionalPluginPool, resolvePluginsRelativeTo } = internalSlotsMap.get(this);
const { additionalPluginPool } = internalSlotsMap.get(this);
const request = naming.normalizePackageName(name, "eslint-plugin");
const id = naming.getShorthandName(request, "eslint-plugin");
const relativeTo = path.join(resolvePluginsRelativeTo, "__placeholder__.js");
const relativeTo = path.join(ctx.pluginBasePath, "__placeholder__.js");

if (name.match(/\s+/u)) {
const error = Object.assign(
Expand Down Expand Up @@ -983,7 +997,7 @@ class ConfigArrayFactory {
error.messageTemplate = "plugin-missing";
error.messageData = {
pluginName: request,
resolvePluginsRelativeTo,
resolvePluginsRelativeTo: ctx.pluginBasePath,
importerName: ctx.name
};
}
Expand Down
28 changes: 28 additions & 0 deletions lib/cli-engine/config-array/config-array.js
Expand Up @@ -156,6 +156,23 @@ function mergeWithoutOverwrite(target, source) {
}
}

/**
* The error for plugin conflicts.
*/
class PluginConflictError extends Error {

/**
* Initialize this error object.
* @param {string} pluginId The plugin ID.
* @param {{filePath:string, importerName:string}[]} plugins The resolved plugins.
*/
constructor(pluginId, plugins) {
super(`Plugin "${pluginId}" was conflicted between ${plugins.map(p => `"${p.importerName}"`).join(" and ")}.`);
this.messageTemplate = "plugin-conflict";
this.messageData = { pluginId, plugins };
}
}

/**
* Merge plugins.
* `target`'s definition is prior to `source`'s.
Expand All @@ -181,6 +198,17 @@ function mergePlugins(target, source) {
throw sourceValue.error;
}
target[key] = sourceValue;
} else if (sourceValue.filePath !== targetValue.filePath) {
throw new PluginConflictError(key, [
{
filePath: targetValue.filePath,
importerName: targetValue.importerName
},
{
filePath: sourceValue.filePath,
importerName: sourceValue.importerName
}
]);
}
}
}
Expand Down
7 changes: 7 additions & 0 deletions messages/plugin-conflict.txt
@@ -0,0 +1,7 @@
ESLint couldn't determine the plugin "<%- pluginId %>" uniquely.
<% for (const { filePath, importerName } of plugins) { %>
- <%= filePath %> (loaded in "<%= importerName %>")<% } %>

Please remove the "plugins" setting from either config or remove either plugin installation.

If you still can't figure out the problem, please stop by https://gitter.im/eslint/eslint to chat with the team.
3 changes: 2 additions & 1 deletion tests/lib/cli-engine/cascading-config-array-factory.js
Expand Up @@ -938,7 +938,8 @@ describe("CascadingConfigArrayFactory", () => {
cliConfig: {
plugins: ["another-plugin"]
},
cwd: getFixturePath("plugins")
cwd: getFixturePath("plugins"),
resolvePluginsRelativeTo: getFixturePath("plugins")
});
const file = getFixturePath("broken", "plugins", "console-wrong-quotes.js");
const expected = {
Expand Down