import-js / eslint-plugin-import

ESLint plugin with rules that help validate proper imports.
MIT License
5.57k stars 1.57k forks source link

`import/no-extraneous-dependencies` gets confused in a monorepo #3017

Open saiichihashimoto opened 5 months ago

saiichihashimoto commented 5 months ago

I'm using a turborepo monorepo and I'm seeing if packages/A/src/a.ts imports from packages/A/src/b.ts AND packages/B/src/a.ts imports from packages/B/src/b.ts, then import/no-extraneous-dependencies errors. It gets mad at package B thinking that, when it imports src/b, it's from package A, even though it isn't.

eslintrc ```json { "$schema": "https://json.schemastore.org/eslintrc.json", "root": true, "ignorePatterns": ["!.*"], "plugins": [ "eslint-comments", "fp", "import", "lodash-fp", "node", "prefer-arrow", "promise", "unicorn" ], "extends": [ "eslint:recommended", "plugin:@cspell/recommended", "plugin:eslint-comments/recommended", "plugin:fp/recommended", "plugin:import/recommended", "plugin:lodash-fp/recommended", "plugin:node/recommended-module", "plugin:promise/recommended", "plugin:unicorn/recommended", "airbnb" ], "parserOptions": { "ecmaVersion": 11, "sourceType": "module" }, "settings": { "import/extensions": [".js", ".jsx", ".ts", ".tsx"], "import/resolver": { "node": { "extensions": [".js", ".jsx", ".ts", ".tsx"] }, "typescript": { "project": [ "./tsconfig.json", "./@types/tsconfig.json", "./packages/*/tsconfig.json" ] } }, "react": { "version": "detect" } }, "env": { "es6": true }, "rules": { "arrow-body-style": ["error", "as-needed"], "curly": "error", "dot-notation": "error", "eqeqeq": "error", "func-style": ["error", "expression"], "max-classes-per-file": "off", "no-buffer-constructor": "off", "no-caller": "error", "no-constant-binary-expression": "error", "no-else-return": "error", "no-eq-null": "error", "no-extend-native": "error", "no-floating-decimal": "error", "no-implicit-coercion": "error", "no-implied-eval": "error", "no-iterator": "error", "no-lonely-if": "error", "no-loop-func": "error", "no-mixed-operators": "error", "no-multi-assign": "error", "no-nested-ternary": "off", "no-param-reassign": "error", "no-plusplus": "error", "no-return-assign": "error", "no-return-await": "error", "no-script-url": "error", "no-self-compare": "error", "no-sequences": "error", "no-shadow": "off", "no-template-curly-in-string": "error", "no-throw-literal": "error", "no-underscore-dangle": "off", "no-unneeded-ternary": "error", "no-unreachable-loop": "error", "no-unused-expressions": "error", "no-unused-private-class-members": "error", "no-unused-vars": [ "error", { "ignoreRestSiblings": true } ], "no-use-before-define": "error", "no-useless-computed-key": "error", "no-useless-concat": "error", "no-useless-return": "error", "no-var": "error", "no-warning-comments": [ "error", { "terms": ["fixme"], "location": "anywhere" } ], "prefer-arrow-callback": "error", "prefer-const": "error", "prefer-destructuring": "error", "prefer-exponentiation-operator": "error", "prefer-numeric-literals": "error", "prefer-object-spread": "error", "prefer-promise-reject-errors": "error", "prefer-regex-literals": "error", "prefer-rest-params": "error", "prefer-spread": "error", "prefer-template": "error", "radix": "error", "require-atomic-updates": "error", "spaced-comment": "error", "yoda": "error", "@cspell/spellchecker": [ "error", { "autoFix": false, "cspell": { "ignoreRegExpList": ["_key: \".*\"", "_rev: \".*\""], "words": [ "bson", "datetime", "exif", "geopoint", "groq", "hotspot", "lqip", "portabletext", "randexp", "stega", "tsup", "unflow", "unstash", "upserted", "zeplo", "zods" ] } } ], "eslint-comments/disable-enable-pair": [ "error", { "allowWholeFile": true } ], "eslint-comments/no-unused-disable": "error", "eslint-comments/require-description": [ "error", { "ignore": ["eslint-enable"] } ], "fp/no-let": "off", "fp/no-mutating-methods": "off", "fp/no-mutation": "off", "fp/no-nil": "off", "fp/no-rest-parameters": "off", "fp/no-throw": "off", "fp/no-unused-expression": "off", "import/extensions": ["error", "never"], "import/no-absolute-path": "error", "import/no-amd": "error", "import/no-anonymous-default-export": "error", "import/no-commonjs": "error", "import/no-cycle": "error", "import/no-deprecated": "error", "import/no-dynamic-require": "error", "import/no-extraneous-dependencies": [ "error", { "devDependencies": false } ], "import/no-mutable-exports": "error", "import/no-relative-packages": "error", "import/no-self-import": "error", "import/no-useless-path-segments": "error", "import/no-webpack-loader-syntax": "error", "import/order": "off", "import/prefer-default-export": "off", "lodash-fp/consistent-compose": ["error", "flow"], "lodash-fp/no-single-composition": "off", "lodash-fp/preferred-alias": [ "error", { "overrides": [] } ], "node/no-extraneous-import": "off", "node/no-missing-import": "off", "node/no-new-require": "error", "node/no-path-concat": "error", "node/no-process-exit": "error", "node/no-unpublished-import": "off", "node/no-unsupported-features/es-syntax": "off", "node/prefer-global/buffer": "error", "node/prefer-global/console": "error", "node/prefer-global/process": "error", "node/prefer-global/text-decoder": "error", "node/prefer-global/text-encoder": "error", "node/prefer-global/url": "error", "node/prefer-global/url-search-params": "error", "node/prefer-promises/dns": "error", "node/prefer-promises/fs": "error", "prefer-arrow/prefer-arrow-functions": [ "error", { "disallowPrototype": true } ], "promise/no-multiple-resolved": "error", "promise/prefer-await-to-callbacks": "error", "promise/prefer-await-to-then": "error", "promise/valid-params": "off", "react/forbid-prop-types": "off", "unicorn/consistent-function-scoping": [ "error", { "checkArrowFunctions": false } ], "unicorn/explicit-length-check": "off", "unicorn/filename-case": [ "error", { "case": "kebabCase", "ignore": ["^\\[.*\\]\\.tsx$"] } ], "unicorn/import-style": "off", "unicorn/no-array-callback-reference": "off", "unicorn/no-array-method-this-argument": "off", "unicorn/no-array-reduce": "off", "unicorn/no-await-expression-member": "off", "unicorn/no-negated-condition": "off", "unicorn/no-null": "off", "unicorn/no-unreadable-array-destructuring": "off", "unicorn/no-unused-properties": "error", "unicorn/no-useless-undefined": "off", "unicorn/numeric-separators-style": "off", "unicorn/prefer-at": "error", "unicorn/prefer-json-parse-buffer": "error", "unicorn/prefer-node-protocol": "off", "unicorn/prefer-string-replace-all": "error", "unicorn/prevent-abbreviations": "off", "unicorn/require-array-join-separator": "off" }, "overrides": [ { "files": ["**/*.js", "**/*.jsx"], "extends": ["plugin:node/recommended-script"], "env": { "commonjs": true, "es6": false }, "rules": { "fp/no-mutation": [ "error", { "commonjs": true } ], "import/no-commonjs": "off", "node/exports-style": ["error", "module.exports"], "unicorn/prefer-module": "off" } }, { "files": ["**/*.ts", "**/*.tsx"], "plugins": ["@typescript-eslint"], "extends": [ "plugin:@typescript-eslint/recommended", "plugin:@typescript-eslint/strict", "plugin:import/typescript", "plugin:typescript-sort-keys/recommended" ], "parser": "@typescript-eslint/parser", "parserOptions": { "project": [ "./tsconfig.json", "./@types/tsconfig.json", "./packages/*/tsconfig.json" ] }, "rules": { "no-loop-func": "off", "no-redeclare": "off", "no-return-await": "off", "no-unused-expressions": "off", "no-unused-vars": "off", "no-use-before-define": "off", "@typescript-eslint/ban-ts-comment": [ "error", { "ts-expect-error": { "descriptionFormat": "^ (TODO|FIXME|EXPECTED) .+$" } } ], "@typescript-eslint/consistent-indexed-object-style": [ "error", "index-signature" ], "@typescript-eslint/consistent-type-definitions": ["error", "type"], "@typescript-eslint/consistent-type-exports": "error", "@typescript-eslint/consistent-type-imports": [ "error", { "fixStyle": "inline-type-imports" } ], "@typescript-eslint/method-signature-style": "error", "@typescript-eslint/no-empty-function": "off", "@typescript-eslint/no-empty-interface": "off", "@typescript-eslint/no-explicit-any": [ "off", { "fixToUnknown": true, "ignoreRestArgs": true } ], "@typescript-eslint/no-loop-func": "error", "@typescript-eslint/no-non-null-asserted-optional-chain": "off", "@typescript-eslint/no-non-null-assertion": "off", "@typescript-eslint/no-redeclare": "error", "@typescript-eslint/no-redundant-type-constituents": "error", "@typescript-eslint/no-require-imports": "error", "@typescript-eslint/no-unused-expressions": "error", "@typescript-eslint/no-unused-vars": [ "error", { "ignoreRestSiblings": true } ], "@typescript-eslint/no-use-before-define": [ "error", { "ignoreTypeReferences": false } ], "@typescript-eslint/no-useless-empty-export": "error", "@typescript-eslint/prefer-regexp-exec": "error", "@typescript-eslint/promise-function-async": "error", "@typescript-eslint/return-await": "error", "@typescript-eslint/sort-type-constituents": "error", "@typescript-eslint/switch-exhaustiveness-check": "error", "import/consistent-type-specifier-style": ["error", "prefer-top-level"] } }, { "files": ["**/*.jsx", "**/*.tsx"], "plugins": ["react", "react-hooks"], "extends": ["plugin:react/recommended", "plugin:react-hooks/recommended"], "parserOptions": { "ecmaFeatures": { "jsx": true } }, "rules": { "react/function-component-definition": [ "error", { "namedComponents": "arrow-function" } ], "react/jsx-filename-extension": [ "error", { "allow": "as-needed", "extensions": [".jsx", ".tsx"] } ], "react/jsx-props-no-spreading": "off", "react/no-danger": "off", "react/no-unstable-nested-components": [ "error", { "allowAsProps": true } ], "react/react-in-jsx-scope": "off" } }, { "files": ["**/*.spec.*", "**/*.test.*"], "extends": [ "plugin:jest-formatting/recommended", "plugin:jest/recommended" ], "env": { "jest": true }, "rules": { "id-length": "off", "@typescript-eslint/consistent-type-imports": "off", "@typescript-eslint/promise-function-async": "off", "import/no-extraneous-dependencies": [ "error", { "devDependencies": true } ], "jest/consistent-test-it": [ "error", { "fn": "it", "withinDescribe": "it" } ], "jest/expect-expect": [ "error", { "assertFunctionNames": ["expect", "expectType"] } ], "jest/no-alias-methods": "error", "jest/no-commented-out-tests": "error", "jest/no-disabled-tests": "error", "jest/no-duplicate-hooks": "error", "jest/no-test-return-statement": "error", "jest/no-untyped-mock-factory": "error", "jest/prefer-called-with": "error", "jest/prefer-comparison-matcher": "error", "jest/prefer-each": "error", "jest/prefer-equality-matcher": "error", "jest/prefer-expect-resolves": "error", "jest/prefer-hooks-in-order": "error", "jest/prefer-hooks-on-top": "error", "jest/prefer-mock-promise-shorthand": "error", "jest/prefer-spy-on": "error", "jest/prefer-strict-equal": "error", "jest/prefer-to-be": "error", "jest/prefer-to-contain": "error", "jest/prefer-to-have-length": "error", "jest/prefer-todo": "error", "jest/require-to-throw-message": "error", "jest/require-top-level-describe": "error", "lodash-fp/no-unused-result": "off", "unicorn/consistent-function-scoping": "off" } }, { "files": ["**/*.d.ts"], "plugins": ["@typescript-eslint"], "parser": "@typescript-eslint/parser", "parserOptions": { "project": [ "./tsconfig.json", "./@types/tsconfig.json", "./packages/*/tsconfig.json" ] }, "rules": { "no-var": "off", "@typescript-eslint/consistent-type-definitions": [ "error", "interface" ], "@typescript-eslint/consistent-type-imports": [ "error", { "disallowTypeAnnotations": false, "fixStyle": "inline-type-imports" } ], "@typescript-eslint/no-explicit-any": "off", "import/no-extraneous-dependencies": "off" } }, { "files": ["**/*"], "extends": ["prettier"] } ] } ```
ljharb commented 5 months ago

Duplicate of #3013.

ljharb commented 5 months ago

Maybe not a duplicate.

erikerikson commented 3 days ago

I am experiencing this too, in a handmade monorepo.

node --version v20.16.0
npm --version 10.8.1
@typescript-eslint/eslint-plugin: ^6.17.0
@typescript-eslint/parser: ^6.21.0
eslint: ^8.57.1
eslint-import-resolver-typescript: ^3.6.3
eslint-plugin-import: ^2.31.0

I have the following paths value in my tsconfig.json:

      "#/*": [
        "/src/org/repo/bootstrap/*",
        "/src/org/repo/src/*",
      ],

Both of these paths exist:

/src/org/repo/src/api
/src/org/repo/src/deploy

The following is one example of a snippet of code (from api) being linted using that path definition inconsistently:

import ServerError from '#/api/lib/errors/ServerError' // works!
import check from '#/deploy/lib/check' // 'deploy' should be listed in the project's dependencies ... import/no-extraneous-dependencies

elsewhere (in deploy), the #/api link fails:

import { noLogSync } from '#/api/lib/util' // fails import/no-extraneous-dependencies

because it is a monorepo, both these sub-project have a tsconfig.json but both extend a tsconfig.json at /src/org/repo/tsconfig.json. That is where the paths value is defined and I used absolute paths in order to avoid the cwd/reference point problem. It doesn't just fail in those projects but I also have a failure in the www sub-project:

import { env } from '#/deploy/lib/config' // fails import/no-extraneous-dependencies

I have the following in my eslint:

      'import/parsers': {
        '@typescript-eslint/parser': ['.cts', '.ctsx', '.mts', '.mtsx', '.ts', '.tsx'],
      },
      'import/resolver': {
        typescript: {
          alwaysTryTypes: true,
          project: [
            './tsconfig.json',
            './bootstrap/*/tsconfig.json',
            './src/*/tsconfig.json',
          ],
        },
      },

in context (/src/org/repo/node_modules/eslint-plugin-import/lib/rules/no-extraneous-dependencies.js#reportIfMissing), typeOfImport is external, importPackageName is # and realPackageName is api/deploy

In that context, it seems that var typeOfImport = (0, _importType2['default'])(name, context) is failing to notice (only in a subset of cases) that # is a defined path/alias to /src/org/repo/src/ but perhaps this context is past the point of the error and I should be looking earlier.

To be fair, #/deploy/... is external to api and #/api/... is external to deploy while both are external to www. However, they are internal to the monorepo and via paths they are "local"/not part of the package.json dependencies.

erikerikson commented 3 days ago

And... in fact, I was able to resolve my issue by adding the following:

...
    settings: {
      'import/internal-regex': '^#',
      ...
    }
...

Sorry for the noise