eslint / eslint

Find and fix problems in your JavaScript code.
https://eslint.org
MIT License
24.44k stars 4.41k forks source link

Bug: Imports fail in flat config file when ESLint installed elsewhere #18465

Closed adamchainz closed 3 days ago

adamchainz commented 2 weeks ago

Environment

Node version: v20.12.0 npm version: 10.8.0 Local ESLint version: v9.2.0 Global ESLint version: n/a Operating System: macOS

What parser are you using?

Default (Espree)

What did you do?

An empty directory with just this eslint.config.js:

const js = require("@eslint/js");

module.exports = [
  js.configs.recommended,
]

Run the CLI with npx as documented:

$ npx eslint

What did you expect to happen?

The require / import statements resolve. @eslint/js is installed with ESLint: https://github.com/eslint/eslint/blob/06f1d1cd874dfc40a6651b08d766f6522a67b3f0/package.json#L72 .

What actually happened?

Crash:

$ npx eslint
Oops! Something went wrong! :(

ESLint: 9.2.0

Error: Cannot find module '@eslint/js'
Require stack:
- /Users/chainz/tmp/test-eslint/eslint.config.js
    at Module._resolveFilename (node:internal/modules/cjs/loader:1143:15)
    at Module._load (node:internal/modules/cjs/loader:984:27)
    at Module.require (node:internal/modules/cjs/loader:1231:19)
    at require (node:internal/modules/helpers:179:18)
    at Object.<anonymous> (/Users/chainz/tmp/test-eslint/eslint.config.js:1:12)
    at Module._compile (node:internal/modules/cjs/loader:1369:14)
    at Module._extensions..js (node:internal/modules/cjs/loader:1427:10)
    at Module.load (node:internal/modules/cjs/loader:1206:32)
    at Module._load (node:internal/modules/cjs/loader:1022:12)
    at cjsLoader (node:internal/modules/esm/translators:366:17)

The way ESLint loads the config file means that its import statements aren’t resolved.

(The bug also occurs with ESM config in eslint.config.mjs, with a slightly different error message: Cannot find package '@eslint/js' imported from /Users/chainz/tmp/test-eslint/eslint.config.mjs.)

Link to Minimal Reproducible Example

Open https://stackblitz.com/edit/stackblitz-starters-jycfg3?file=eslint.config.js and run npx eslint:

Screenshot 2024-05-20 at 13-29-33 eslint #18465 - StackBlitz

Participation

Additional comments

It seems that to use flat config, you must install ESLint in the same directory as configuration? If so, that really breaks using tool managers—I found this out when using ESLint under pre-commit, which manages tools in its own directory outside of your project.

nzakas commented 2 weeks ago

As the issue template says, we can't investigate any bug reports without a repro case. If you can please create a StackBlitz showing this behavior we can take a look.

adamchainz commented 1 week ago

Link: https://stackblitz.com/edit/stackblitz-starters-jycfg3?file=eslint.config.js

Screenshot of repro on stackblitz:

Screenshot 2024-05-20 at 13-29-33 eslint #18465 - StackBlitz

nzakas commented 1 week ago

Ah okay, I see what's happening.

Yes, you still need to install @eslint/js into your project manually (as you would any other import that you have in a config file). Even though @eslint/js is a dependency of ESLint, that doesn't mean you can use it directly without installing it first.

sritchie commented 1 day ago

@nzakas, I have hit this issue as well when trying to use eslint with https://pre-commit.ci/. The issue here is that pre-commit.ci installs eslint "globally" and won't run npm install. The plugin takes a list of extra dependencies and installs them into npm's virtual environment. If eslint used this global node_modules folder all would work, but eslint fails to find any dependencies in the local folder on pre-commit.ci because npm install was never run.

Is there some way to tell eslint to look for dependencies elsewhere? I know that in normal use you don't want to support this, but I think for a CI environment being able to somehow add another source of node_modules is essential.

It seems that eslint used to take a --resolve-plugins-relative-to flag, but no longer does; is there some way to get behavior like this back, even if's hacky?

Thank you!

nzakas commented 1 day ago

@sritchie use NODE_PATH

sritchie commented 1 day ago

@nzakas I tried this again and confirmed that NODE_PATH is set correctly. I created a reproduction here with details:

https://github.com/sritchie/eslint-repro

Here are the details, also in the repro README. (The implicit question here is, is this

In the repro, .pre-commit-config.yaml looks like this:

repos:
  - repo: local
    hooks:
    - id: eslint
      name: eslint
      language: node
      additional_dependencies:
      - eslint@9.3.0
      - globals@15.3.0
      entry: ./run_eslint.sh

This will install a node environment with the listed "additional dependencies" (installed via npm install), and then trigger the entry point.

I used a shell script here, run_eslint.sh, so I could show that the environment variables are set correctly, and that the NODE_PATH does indeed have eslint and globals installed into it.

However, when I either run the script explicitly:

./run_eslint.sh

Or run it through pre-commit:

pre-commit run eslint --all

I see the following output:

NODE_PATH:
/Users/sritchie/.cache/pre-commit/repoxz3rawvg/node_env-default/lib/node_modules
NPM_CONFIG_PREFIX:
/Users/sritchie/.cache/pre-commit/repoxz3rawvg/node_env-default
contents of NODE_PATH:
corepack eslint globals npm pre_commit_placeholder_package

Oops! Something went wrong! :(

ESLint: 9.3.0

Error [ERR_MODULE_NOT_FOUND]: Cannot find package 'globals' imported from /Users/sritchie/code/mit/eslint-repro/eslint.config.mjs
Did you mean to import "globals/index.js"?
    at packageResolve (node:internal/modules/esm/resolve:841:9)
    at moduleResolve (node:internal/modules/esm/resolve:914:18)
    at defaultResolve (node:internal/modules/esm/resolve:1119:11)
    at ModuleLoader.defaultResolve (node:internal/modules/esm/loader:542:12)
    at ModuleLoader.resolve (node:internal/modules/esm/loader:511:25)
    at ModuleLoader.getModuleJob (node:internal/modules/esm/loader:241:38)
    at ModuleJob._link (node:internal/modules/esm/module_job:126:49)

telling me that NODE_PATH is set to a directory containing globals, but eslint can't find the import in eslint.config.mjs.

I would have expected that this eslint.config.mjs should work fine, given that globals is installed:

import globals from "globals";

export default [
  {
    languageOptions: { globals: globals.browser },
    files: ["src/**/*.js"],
  },
];