Skip to content

eslint-import-resolver-node's resolution does not match node's with respect to symlinks #3110

Open
@anomiex

Description

@anomiex

Consider this example project, structured similarly to what pnpm produces: t.tar.gz

The directory structure looks something like this:

t/
t/index.js
t/node_modules/
t/node_modules/b -> .pnpm/b@1.0.0/node_modules/b
t/node_modules/.pnpm/
t/node_modules/.pnpm/b@1.0.0/
t/node_modules/.pnpm/b@1.0.0/node_modules/
t/node_modules/.pnpm/b@1.0.0/node_modules/b/
t/node_modules/.pnpm/b@1.0.0/node_modules/b/index.js
t/node_modules/.pnpm/b@1.0.0/node_modules/b/package.json
t/node_modules/.pnpm/b@1.0.0/node_modules/c -> ../../c@1.0.0/node_modules/c
t/node_modules/.pnpm/c@1.0.0/
t/node_modules/.pnpm/c@1.0.0/node_modules/
t/node_modules/.pnpm/c@1.0.0/node_modules/c/
t/node_modules/.pnpm/c@1.0.0/node_modules/c/index.js
t/node_modules/.pnpm/c@1.0.0/node_modules/c/package.json
t/package.json

Of particular note, package b depends on c, and pnpm structures things as above so that t/index.js won't see c as a phantom dependency. c exports a named export 'foo', and b rexports that with export * from 'c'.

When t/index.js imports b, standard Node module resoluton resolves that via the symlink t/node_modules/b, returning the absolute realpath /some/absolute/path/t/node_modules/.pnpm/b@1.0.0/node_modules/b/index.js. Resolving c relative to that path finds it via the symlink t/node_modules/.pnpm/b@1.0.0/node_modules/c.

eslint-import-resolver-node's resolution differs in one key respect: by default, instead of returning the absolute realpath /some/absolute/path/t/node_modules/.pnpm/b@1.0.0/node_modules/b/index.js, it returns /some/absolute/path/t/node_modules/b/index.js instead. It then (correctly) cannot resolve c relative to that path.

A relevant eslint.config.js to use with the above could look like this:

import importPlugin from 'eslint-plugin-import';

export default [ importPlugin.flatConfigs.recommended ];

which will produce the following false positive due to this bug

  1:10  error  foo not found in 'b'  import/named

If you want a "real" reproduction, starting in an empty project:

  1. Create a package.json with "type": "module" (or use extension .mjs instead of .js in steps 3 and 4).
  2. Run pnpm add eslint eslint-plugin-import @playwright/test.
  3. Create the above eslint.config.js.
  4. Create a JS file containing import { expect } from '@playwright/test';.
  5. Run eslint on that file.

As for a fix, it looks like setting preserveSymlinks: false in

function opts(file, config, packageFilter) {
return Object.assign({ // more closely matches Node (#333)
// plus 'mjs' for native modules! (#939)
extensions: ['.mjs', '.js', '.json', '.node'],
}, config, {
// path.resolve will handle paths relative to CWD
basedir: path.dirname(path.resolve(file)),
packageFilter,
});
}
should do it. As a workaround this can be set in the eslint config with

{
    settings: {
        'import/resolver': {
            node: {
                preserveSymlinks: false,
            },
        }
    }
}

I note the README for the resolve package you use recommends setting preserveSymlinks: false to match Node's behavior; as far as I can tell, preserving symlinks was only the default in Node for a short time between 6.0.0 and 6.2.0, although the documentation as to the expected behavior was unclear before that.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions