Skip to content

Commit

Permalink
Breaking: simplify config/plugin/parser resolution (fixes #10125)
Browse files Browse the repository at this point in the history
This change updates ESLint to load plugins relative to the user's project root, and other packages relative to where they're specified in a config. This simplifies ESLint's package-loading, resulting in fewer confusing errors about missing packages. It also fixes an existing design bug where ESLint would sometimes fail to load plugins in valid setups.

Implements eslint/rfcs#7.
  • Loading branch information
not-an-aardvark committed Feb 22, 2019
1 parent eb0650b commit 8df9633
Show file tree
Hide file tree
Showing 51 changed files with 710 additions and 1,341 deletions.
41 changes: 3 additions & 38 deletions README.md
Expand Up @@ -28,11 +28,7 @@ ESLint is a tool for identifying and reporting on patterns found in ECMAScript/J

Prerequisites: [Node.js](https://nodejs.org/en/) (>=6.14), npm version 3+.

There are two ways to install ESLint: globally and locally.

### Local Installation and Usage

If you want to include ESLint as part of your project's build system, we recommend installing it locally. You can do so using npm:
You can install ESLint using npm:

```
$ npm install eslint --save-dev
Expand All @@ -50,31 +46,7 @@ After that, you can run ESLint on any file or directory like this:
$ ./node_modules/.bin/eslint yourfile.js
```

Any plugins or shareable configs that you use must also be installed locally to work with a locally-installed ESLint.

### Global Installation and Usage

If you want to make ESLint available to tools that run across all of your projects, we recommend installing ESLint globally. You can do so using npm:

```
$ npm install -g eslint
```

You should then set up a configuration file:

```
$ eslint --init
```

After that, you can run ESLint on any file or directory like this:

```
$ eslint yourfile.js
```

Any plugins or shareable configs that you use must also be installed globally to work with a globally-installed ESLint.

**Note:** `eslint --init` is intended for setting up and configuring ESLint on a per-project basis and will perform a local installation of ESLint and its plugins in the directory in which it is run. If you prefer using a global installation of ESLint, any plugins used in your configuration must also be installed globally.
It is also possible to install ESLint globally rather than locally (using `npm install eslint --global`). However, any plugins or shareable configs that you use must be installed locally in either case.

## Configuration

Expand Down Expand Up @@ -124,16 +96,9 @@ No, ESLint does both traditional linting (looking for problematic patterns) and

### Why can't ESLint find my plugins?

ESLint can be [globally or locally installed](#installation-and-usage). If you install ESLint globally, your plugins must also be installed globally; if you install ESLint locally, your plugins must also be installed locally.

If you are trying to run globally, make sure your plugins are installed globally (use `npm ls -g`).

If you are trying to run locally:

* Make sure your plugins (and ESLint) are both in your project's `package.json` as devDependencies (or dependencies, if your project uses ESLint at runtime).
* Make sure you have run `npm install` and all your dependencies are installed.

In all cases, make sure your plugins' peerDependencies have been installed as well. You can use `npm view eslint-plugin-myplugin peerDependencies` to see what peer dependencies `eslint-plugin-myplugin` has.
* Make sure your plugins' peerDependencies have been installed as well. You can use `npm view eslint-plugin-myplugin peerDependencies` to see what peer dependencies `eslint-plugin-myplugin` has.

### Does ESLint support JSX?

Expand Down
6 changes: 2 additions & 4 deletions docs/developer-guide/nodejs-api.md
Expand Up @@ -273,10 +273,8 @@ Map {

### Linter#defineParser

Each instance of `Linter` holds a map of custom parsers. If you want to define a parser programmatically you can add this function
with the name of the parser as first argument and the [parser object](/docs/developer-guide/working-with-plugins.md#working-with-custom-parsers) as second argument.

If during linting the parser is not found, it will fallback to `require(parserId)`.
Each instance of `Linter` holds a map of custom parsers. If you want to define a parser programmatically, you can add this function
with the name of the parser as first argument and the [parser object](/docs/developer-guide/working-with-plugins.md#working-with-custom-parsers) as second argument. The default `"espree"` parser will already be loaded for every `Linter` instance.

```js
const Linter = require("eslint").Linter;
Expand Down
2 changes: 2 additions & 0 deletions docs/developer-guide/shareable-configs.md
Expand Up @@ -38,6 +38,8 @@ You should declare your dependency on ESLint in `package.json` using the [peerDe
}
```

If your shareable config depends on a plugin, you should also specify it as a `peerDependency` (plugins are be loaded relative to the end user's project, so the end user is required to install the plugins they need). However, if your shareable config depends on a third-party parser or another shareable config, you can specify these packages as `dependencies`.

You can also test your shareable config on your computer before publishing by linking your module globally. Type:

```bash
Expand Down
17 changes: 7 additions & 10 deletions docs/user-guide/configuring.md
Expand Up @@ -60,9 +60,8 @@ Setting parser options helps ESLint determine what is a parsing error. All langu

By default, ESLint uses [Espree](https://github.com/eslint/espree) as its parser. You can optionally specify that a different parser should be used in your configuration file so long as the parser meets the following requirements:

1. It must be an npm module installed locally.
1. It must have an Esprima-compatible interface (it must export a `parse()` method).
1. It must produce Esprima-compatible AST and token objects.
1. It must be a Node module loadable from the config file where it appears. Usually, this means you should install the parser package separately from npm.
1. It must conform to the [parser interface](/docs/developer-guide/working-with-plugins.md#working-with-custom-parsers).

Note that even with these compatibilities, there are no guarantees that an external parser will work correctly with ESLint and ESLint will not fix bugs related to incompatibilities with other parsers.

Expand Down Expand Up @@ -255,7 +254,7 @@ For historical reasons, the boolean value `false` and the string value `"readabl

## Configuring Plugins

ESLint supports the use of third-party plugins. Before using the plugin you have to install it using npm.
ESLint supports the use of third-party plugins. Before using the plugin, you have to install it using npm.

To configure plugins inside of a configuration file, use the `plugins` key, which contains a list of plugin names. The `eslint-plugin-` prefix can be omitted from the plugin name.

Expand All @@ -277,7 +276,7 @@ And in YAML:
- eslint-plugin-plugin2
```

**Note:** Due to the behavior of Node's `require` function, a globally-installed instance of ESLint can only use globally-installed ESLint plugins, and locally-installed version can only use *locally-installed* plugins. Mixing local and global plugins is not supported.
**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.

## Configuring Rules

Expand Down Expand Up @@ -625,10 +624,10 @@ A configuration file can extend the set of enabled rules from base configuration

The `extends` property value is either:

* a string that specifies a configuration
* a string that specifies a configuration (either a path to a config file, the name of a shareable config, `eslint:recommended`, or `eslint:all`)
* an array of strings: each additional configuration extends the preceding configurations

ESLint extends configurations recursively so a base configuration can also have an `extends` property.
ESLint extends configurations recursively, so a base configuration can also have an `extends` property. Relative paths and shareable config names in an `extends` property are resolved from the location of the config file where they appear.

The `rules` property can do any of the following to extend (or override) the set of rules:

Expand Down Expand Up @@ -723,9 +722,7 @@ Example of a configuration file in JSON format:

### Using a configuration file

The `extends` property value can be an absolute or relative path to a base [configuration file](#using-configuration-files).

ESLint resolves a relative path to a base configuration file relative to the configuration file that uses it **unless** that file is in your home directory or a directory that isn't an ancestor to the directory in which ESLint is installed (either locally or globally). In those cases, ESLint resolves the relative path to the base file relative to the linted **project** directory (typically the current working directory).
The `extends` property value can be an absolute or relative path to a base [configuration file](#using-configuration-files). ESLint resolves a relative path to a base configuration file relative to the configuration file that uses it.

Example of a configuration file in JSON format:

Expand Down
44 changes: 4 additions & 40 deletions docs/user-guide/getting-started.md
Expand Up @@ -15,61 +15,25 @@ ESLint is a tool for identifying and reporting on patterns found in ECMAScript/J

Prerequisites: [Node.js](https://nodejs.org/en/) (>=6.14), npm version 3+.

There are two ways to install ESLint: globally and locally.

### Local Installation and Usage

If you want to include ESLint as part of your project's build system, we recommend installing it locally. You can do so using npm:
You can install ESLint using npm:

```
$ npm install eslint --save-dev
```

You should then setup a configuration file:
You should then set up a configuration file:

```
$ ./node_modules/.bin/eslint --init
```

After that, you can run ESLint in your project's root directory like this:

```
$ ./node_modules/.bin/eslint yourfile.js
```

Instead of navigating to `./node_modules/.bin/` you may also use `npx` to run `eslint`:

```
$ npx eslint
```

**Note:** If ESLint wasn't manually installed (via `npm`), `npx` will install `eslint` to a temporary directory and execute it.

Any plugins or shareable configs that you use must also be installed locally to work with a locally-installed ESLint.

### Global Installation and Usage

If you want to make ESLint available to tools that run across all of your projects, we recommend installing ESLint globally. You can do so using npm:

```
$ npm install -g eslint
```

You should then setup a configuration file:

```
$ eslint --init
```

After that, you can run ESLint on any file or directory like this:

```
$ eslint yourfile.js
$ ./node_modules/.bin/eslint yourfile.js
```

Any plugins or shareable configs that you use must also be installed globally to work with a globally-installed ESLint.

**Note:** `eslint --init` is intended for setting up and configuring ESLint on a per-project basis and will perform a local installation of ESLint and its plugins in the directory in which it is run. If you prefer using a global installation of ESLint, any plugins used in your configuration must also be installed globally.
It is also possible to install ESLint globally rather than locally (using `npm install eslint --global`). However, any plugins or shareable configs that you use must be installed locally in either case.

## Configuration

Expand Down
34 changes: 28 additions & 6 deletions lib/cli-engine.js
Expand Up @@ -27,13 +27,12 @@ const fs = require("fs"),
globUtils = require("./util/glob-utils"),
validator = require("./config/config-validator"),
hash = require("./util/hash"),
ModuleResolver = require("./util/module-resolver"),
relativeModuleResolver = require("./util/relative-module-resolver"),
naming = require("./util/naming"),
pkg = require("../package.json"),
loadRules = require("./load-rules");

const debug = require("debug")("eslint:cli-engine");
const resolver = new ModuleResolver();
const validFixTypes = new Set(["problem", "suggestion", "layout"]);

//------------------------------------------------------------------------------
Expand Down Expand Up @@ -184,6 +183,13 @@ function processText(text, configHelper, filename, fix, allowInlineConfig, repor
configHelper.plugins.loadAll(config.plugins);
}

if (config.parser) {
if (!path.isAbsolute(config.parser)) {
throw new Error(`Expected parser to be an absolute path but found ${config.parser}. This is a bug.`);
}
linter.defineParser(config.parser, require(config.parser));
}

const loadedPlugins = configHelper.plugins.getAll();

for (const plugin in loadedPlugins) {
Expand Down Expand Up @@ -463,7 +469,23 @@ class CLIEngine {
});
}

this.config = new Config(this.options, this.linter);
this.config = new Config(
{
cwd: this.options.cwd,
baseConfig: this.options.baseConfig,
rules: this.options.rules,
ignore: this.options.ignore,
ignorePath: this.options.ignorePath,
parser: this.options.parser,
parserOptions: this.options.parserOptions,
useEslintrc: this.options.useEslintrc,
envs: this.options.envs,
globals: this.options.globals,
configFile: this.options.configFile,
plugins: this.options.plugins
},
this.linter
);

if (this.options.cache) {
const cacheFile = getCacheFile(this.options.cacheLocation || this.options.cacheFile, this.options.cwd);
Expand Down Expand Up @@ -764,16 +786,16 @@ class CLIEngine {

let formatterPath;

// if there's a slash, then it's a file
// if there's a slash, then it's a file (TODO: this check seems dubious for scoped npm packages)
if (!namespace && normalizedFormatName.indexOf("/") > -1) {
formatterPath = path.resolve(cwd, normalizedFormatName);
} else {
try {
const npmFormat = naming.normalizePackageName(normalizedFormatName, "eslint-formatter");

formatterPath = resolver.resolve(npmFormat, `${cwd}/node_modules`);
formatterPath = relativeModuleResolver(npmFormat, path.join(cwd, "placeholder.js"));
} catch (e) {
formatterPath = `./formatters/${normalizedFormatName}`;
formatterPath = path.resolve(__dirname, "formatters", normalizedFormatName);
}
}

Expand Down

0 comments on commit 8df9633

Please sign in to comment.