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-unused-modules doesn't work in monorepo #3013

Open saiichihashimoto opened 5 months ago

saiichihashimoto commented 5 months ago

I'm using a turborepo monorepo and I'm seeing if packages/A/a.ts imports from packages/B/b.ts, the b.ts will have it's exports marked as "not used within other modules".

ljharb commented 5 months ago

what's your eslint config?

saiichihashimoto commented 5 months ago
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"] } ] } ```
timostroehlein commented 4 months ago

I'm having the exact same issue in an Nx monorepo with the import/no-extraneous-dependencies rule

saiichihashimoto commented 4 months ago

@timostroehlein me too! I made an issue for that but, for whatever reason, it got marked as a duplicate of this one.

ljharb commented 4 months ago

@saiichihashimoto are they different issues? It read like they were the same.

saiichihashimoto commented 4 months ago

Maybe they come from the same problem, but they're two different eslint rules

timostroehlein commented 4 months ago

Yeah it's the same issue but for two different rules, so it might be caused by an underlying issue. I'm not using the import/no-unused-modules rule, so I can't say anything regarding that

saiichihashimoto commented 1 month ago

I'd assume the import/resolver should have picked up my tsconfigs and been fine. I use the compilerOptions.paths, it seems like that might be where it can't follow anymore? Either way, my repo is littered with // eslint-disable-next-line import/no-unused-modules -- TODO https://github.com/import-js/eslint-plugin-import/issues/3013 from this, so lmk if there's any additional information I can provide.

saiichihashimoto commented 3 weeks ago

Would it help to have a minimal repro gist? How can I help?

michaelfaith commented 3 weeks ago

Curious if it works when you don't have the tsconfig path aliases?

ljharb commented 3 weeks ago

@saiichihashimoto yes, it would help.

saiichihashimoto commented 2 weeks ago

Image

So this ended up solving my issue (albeit, causing many other ones). I noticed that, if I ran the linter before building, it acted as expected but didn't after running a build. I believe, since it's using the typescript resolver, it picks up those fields to include regardless of include and exclude in a tsconfig.json. Since in a monorepo the built packages live next to the source for each package, the including package will pick up the built files instead of src, so then all the src seem unused.

Example for clarity:

packages
- a
  - package.json
  - tsconfig.json
  - src
    - index.ts
  - dist (ONLY after being built)
    - index.js
    - index.d.ts
- b
  - package.json
  - tsconfig.json
  - src
    - index.ts
  - dist (ONLY after being built)
    - index.js
    - index.d.ts

Assume packages/b/src/index.ts imports @monorepo/a and that packages/a/package.json has the main, types, and files as I have in the screenshot.

Before building, packages/b/src/index.ts will resolve @monorepo/a to packages/a/src/index.ts, which is great! After a build however, it will resolve @monorepo/a to packages/a/dist/index.ts which is bad because ALL of packages/a/src/index.ts will be considered unused, even though a linter shouldn't concern itself with built files.

With private packages, deleting those lines in package.json are fine, because nothing needs them. Also, when you're not in a monorepo you can leave them and be fine, because you don't have anything attempting to import the package itself.

But in a monorepo with public packages (ie, packages meant to be published) this is unavoidable. We can't delete those lines because npm needs them. We can't fix it in tsconfig.json because, regardless of what's in the include or exclude, everything listed in files is always included. Also, it seems like this must be solvable because, somehow, VSCode knows to resolve those imports to the source files regardless of whether the files are built or not so, whatever they do, it's theoretically replicate-able here.

ljharb commented 2 weeks ago

That thorough analysis seems like a very plausible path forward. Anyone who has time to figure out how they differentiate, and make a PR, would be appreciated.