From 22b4e2ae1a40928545e914f8dbba99b268ffe6a1 Mon Sep 17 00:00:00 2001 From: SimeonC <1085899+SimeonC@users.noreply.github.com> Date: Wed, 25 Oct 2023 11:13:35 +0900 Subject: [PATCH] fix: eslint rules around imports * Fixed more edge cases for shortestImport. * Added naming convention for imports * made lint executor set type aware options manually --- .nvmrc | 2 +- auditjs.json | 35 ++ package-lock.json | 196 ++++++----- packages/eslint-config/package.json | 14 +- .../src/overrides/documentation.ts | 1 + packages/eslint-config/src/overrides/tests.ts | 1 + .../src/overrides/typescriptTests.ts | 2 + .../src/rules/namingConvention.ts | 4 + packages/eslint-config/src/rules/react.ts | 2 +- .../__tests__/consistentReactImport.test.ts | 4 +- .../__tests__/{ => fixtures}/test_src/.env | 0 .../{ => fixtures}/test_src/default.tsx | 0 .../{ => fixtures}/test_src/feature1/index.ts | 0 .../test_src/feature1/slice1/index.ts | 0 .../test_src/feature1/slice1/inner1/index.ts | 0 .../test_src/feature1/slice1/inner2/index.ts | 0 .../test_src/feature1/slice1/second.ts | 0 .../test_src/feature1/slice2/index.ts | 0 .../test_src/feature1/slice2/second.ts | 0 .../{ => fixtures}/test_src/feature2/index.ts | 0 .../test_src/feature2/sliceA/index.ts | 0 .../fixtures/test_src/tsconfig.test_root.json | 12 + .../fixtures/tsconfig.base_only.json | 9 + .../{ => fixtures}/tsconfig.test.json | 2 +- .../fixtures/tsconfig.test_extends.json | 12 + .../__tests__/forbiddenImports.test.ts | 4 +- .../__tests__/shortestImport.test.ts | 317 +++++++++++------- .../__tests__/tsconfig.base.json | 8 + packages/eslint-plugin/src/shortestImport.ts | 228 ++++++++----- packages/nx/src/executors/quality/executor.ts | 1 + 30 files changed, 554 insertions(+), 300 deletions(-) rename packages/eslint-plugin/__tests__/{ => fixtures}/test_src/.env (100%) rename packages/eslint-plugin/__tests__/{ => fixtures}/test_src/default.tsx (100%) rename packages/eslint-plugin/__tests__/{ => fixtures}/test_src/feature1/index.ts (100%) rename packages/eslint-plugin/__tests__/{ => fixtures}/test_src/feature1/slice1/index.ts (100%) rename packages/eslint-plugin/__tests__/{ => fixtures}/test_src/feature1/slice1/inner1/index.ts (100%) rename packages/eslint-plugin/__tests__/{ => fixtures}/test_src/feature1/slice1/inner2/index.ts (100%) rename packages/eslint-plugin/__tests__/{ => fixtures}/test_src/feature1/slice1/second.ts (100%) rename packages/eslint-plugin/__tests__/{ => fixtures}/test_src/feature1/slice2/index.ts (100%) rename packages/eslint-plugin/__tests__/{ => fixtures}/test_src/feature1/slice2/second.ts (100%) rename packages/eslint-plugin/__tests__/{ => fixtures}/test_src/feature2/index.ts (100%) rename packages/eslint-plugin/__tests__/{ => fixtures}/test_src/feature2/sliceA/index.ts (100%) create mode 100644 packages/eslint-plugin/__tests__/fixtures/test_src/tsconfig.test_root.json create mode 100644 packages/eslint-plugin/__tests__/fixtures/tsconfig.base_only.json rename packages/eslint-plugin/__tests__/{ => fixtures}/tsconfig.test.json (82%) create mode 100644 packages/eslint-plugin/__tests__/fixtures/tsconfig.test_extends.json create mode 100644 packages/eslint-plugin/__tests__/tsconfig.base.json diff --git a/.nvmrc b/.nvmrc index 8ddbc0c6..af9754bc 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v18.16.0 +v20.8.1 diff --git a/auditjs.json b/auditjs.json index e8567d9d..2d7aa065 100644 --- a/auditjs.json +++ b/auditjs.json @@ -1245,6 +1245,38 @@ } ] }, + { + "coordinates": "pkg:npm/axios@1.5.1", + "description": "Promise based HTTP client for the browser and node.js", + "reference": "https://ossindex.sonatype.org/component/pkg:npm/axios@1.5.1?utm_source=auditjs&utm_medium=integration&utm_content=4.0.41", + "vulnerabilities": [ + { + "id": "CVE-2023-45857", + "title": "[CVE-2023-45857] CWE-352: Cross-Site Request Forgery (CSRF)", + "description": "axios - Cross-Site Request Forgery (CSRF)", + "cvssScore": 6.8, + "cvssVector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:N", + "cve": "CVE-2023-45857", + "reference": "https://ossindex.sonatype.org/vulnerability/CVE-2023-45857?component-type=npm&component-name=axios&utm_source=auditjs&utm_medium=integration&utm_content=4.0.41" + } + ] + }, + { + "coordinates": "pkg:npm/axios@1.1.3", + "description": "Promise based HTTP client for the browser and node.js", + "reference": "https://ossindex.sonatype.org/component/pkg:npm/axios@1.1.3?utm_source=auditjs&utm_medium=integration&utm_content=4.0.41", + "vulnerabilities": [ + { + "id": "CVE-2023-45857", + "title": "[CVE-2023-45857] CWE-352: Cross-Site Request Forgery (CSRF)", + "description": "axios - Cross-Site Request Forgery (CSRF)", + "cvssScore": 6.8, + "cvssVector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:N", + "cve": "CVE-2023-45857", + "reference": "https://ossindex.sonatype.org/vulnerability/CVE-2023-45857?component-type=npm&component-name=axios&utm_source=auditjs&utm_medium=integration&utm_content=4.0.41" + } + ] + }, { "coordinates": "pkg:npm/postcss@7.0.39", "description": "Tool for transforming styles with JS plugins", @@ -1485,6 +1517,9 @@ { "id": "CVE-2022-25883" }, + { + "id": "CVE-2023-45857" + }, { "id": "CVE-2023-44270" } diff --git a/package-lock.json b/package-lock.json index 07dd7a26..ce17907d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9703,15 +9703,15 @@ "integrity": "sha512-axdPBuLuEJt0c4yI5OZssC19K2Mq1uKdrfZBzuxLvaztgqUtFYZUNw7lETExPYJR9jdEoIg4mb7RQKRQzOkeGQ==" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "6.7.3", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.7.3.tgz", - "integrity": "sha512-vntq452UHNltxsaaN+L9WyuMch8bMd9CqJ3zhzTPXXidwbf5mqqKCVXEuvRZUqLJSTLeWE65lQwyXsRGnXkCTA==", + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.9.0.tgz", + "integrity": "sha512-lgX7F0azQwRPB7t7WAyeHWVfW1YJ9NIgd9mvGhfQpRY56X6AVf8mwM8Wol+0z4liE7XX3QOt8MN1rUKCfSjRIA==", "dependencies": { "@eslint-community/regexpp": "^4.5.1", - "@typescript-eslint/scope-manager": "6.7.3", - "@typescript-eslint/type-utils": "6.7.3", - "@typescript-eslint/utils": "6.7.3", - "@typescript-eslint/visitor-keys": "6.7.3", + "@typescript-eslint/scope-manager": "6.9.0", + "@typescript-eslint/type-utils": "6.9.0", + "@typescript-eslint/utils": "6.9.0", + "@typescript-eslint/visitor-keys": "6.9.0", "debug": "^4.3.4", "graphemer": "^1.4.0", "ignore": "^5.2.4", @@ -9737,12 +9737,12 @@ } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/scope-manager": { - "version": "6.7.3", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.7.3.tgz", - "integrity": "sha512-wOlo0QnEou9cHO2TdkJmzF7DFGvAKEnB82PuPNHpT8ZKKaZu6Bm63ugOTn9fXNJtvuDPanBc78lGUGGytJoVzQ==", + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.9.0.tgz", + "integrity": "sha512-1R8A9Mc39n4pCCz9o79qRO31HGNDvC7UhPhv26TovDsWPBDx+Sg3rOZdCELIA3ZmNoWAuxaMOT7aWtGRSYkQxw==", "dependencies": { - "@typescript-eslint/types": "6.7.3", - "@typescript-eslint/visitor-keys": "6.7.3" + "@typescript-eslint/types": "6.9.0", + "@typescript-eslint/visitor-keys": "6.9.0" }, "engines": { "node": "^16.0.0 || >=18.0.0" @@ -9753,12 +9753,12 @@ } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/type-utils": { - "version": "6.7.3", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.7.3.tgz", - "integrity": "sha512-Fc68K0aTDrKIBvLnKTZ5Pf3MXK495YErrbHb1R6aTpfK5OdSFj0rVN7ib6Tx6ePrZ2gsjLqr0s98NG7l96KSQw==", + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.9.0.tgz", + "integrity": "sha512-XXeahmfbpuhVbhSOROIzJ+b13krFmgtc4GlEuu1WBT+RpyGPIA4Y/eGnXzjbDj5gZLzpAXO/sj+IF/x2GtTMjQ==", "dependencies": { - "@typescript-eslint/typescript-estree": "6.7.3", - "@typescript-eslint/utils": "6.7.3", + "@typescript-eslint/typescript-estree": "6.9.0", + "@typescript-eslint/utils": "6.9.0", "debug": "^4.3.4", "ts-api-utils": "^1.0.1" }, @@ -9779,9 +9779,9 @@ } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/types": { - "version": "6.7.3", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.7.3.tgz", - "integrity": "sha512-4g+de6roB2NFcfkZb439tigpAMnvEIg3rIjWQ+EM7IBaYt/CdJt6em9BJ4h4UpdgaBWdmx2iWsafHTrqmgIPNw==", + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.9.0.tgz", + "integrity": "sha512-+KB0lbkpxBkBSiVCuQvduqMJy+I1FyDbdwSpM3IoBS7APl4Bu15lStPjgBIdykdRqQNYqYNMa8Kuidax6phaEw==", "engines": { "node": "^16.0.0 || >=18.0.0" }, @@ -9791,12 +9791,12 @@ } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/typescript-estree": { - "version": "6.7.3", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.7.3.tgz", - "integrity": "sha512-YLQ3tJoS4VxLFYHTw21oe1/vIZPRqAO91z6Uv0Ss2BKm/Ag7/RVQBcXTGcXhgJMdA4U+HrKuY5gWlJlvoaKZ5g==", + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.9.0.tgz", + "integrity": "sha512-NJM2BnJFZBEAbCfBP00zONKXvMqihZCrmwCaik0UhLr0vAgb6oguXxLX1k00oQyD+vZZ+CJn3kocvv2yxm4awQ==", "dependencies": { - "@typescript-eslint/types": "6.7.3", - "@typescript-eslint/visitor-keys": "6.7.3", + "@typescript-eslint/types": "6.9.0", + "@typescript-eslint/visitor-keys": "6.9.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -9817,16 +9817,16 @@ } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils": { - "version": "6.7.3", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.7.3.tgz", - "integrity": "sha512-vzLkVder21GpWRrmSR9JxGZ5+ibIUSudXlW52qeKpzUEQhRSmyZiVDDj3crAth7+5tmN1ulvgKaCU2f/bPRCzg==", + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.9.0.tgz", + "integrity": "sha512-5Wf+Jsqya7WcCO8me504FBigeQKVLAMPmUzYgDbWchINNh1KJbxCgVya3EQ2MjvJMVeXl3pofRmprqX6mfQkjQ==", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@types/json-schema": "^7.0.12", "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "6.7.3", - "@typescript-eslint/types": "6.7.3", - "@typescript-eslint/typescript-estree": "6.7.3", + "@typescript-eslint/scope-manager": "6.9.0", + "@typescript-eslint/types": "6.9.0", + "@typescript-eslint/typescript-estree": "6.9.0", "semver": "^7.5.4" }, "engines": { @@ -9841,11 +9841,11 @@ } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/visitor-keys": { - "version": "6.7.3", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.7.3.tgz", - "integrity": "sha512-HEVXkU9IB+nk9o63CeICMHxFWbHWr3E1mpilIQBe9+7L/lH97rleFLVtYsfnWB+JVMaiFnEaxvknvmIzX+CqVg==", + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.9.0.tgz", + "integrity": "sha512-dGtAfqjV6RFOtIP8I0B4ZTBRrlTT8NHHlZZSchQx3qReaoDeXhYM++M4So2AgFK9ZB0emRPA6JI1HkafzA2Ibg==", "dependencies": { - "@typescript-eslint/types": "6.7.3", + "@typescript-eslint/types": "6.9.0", "eslint-visitor-keys": "^3.4.1" }, "engines": { @@ -9857,14 +9857,14 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "6.7.3", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.7.3.tgz", - "integrity": "sha512-TlutE+iep2o7R8Lf+yoer3zU6/0EAUc8QIBB3GYBc1KGz4c4TRm83xwXUZVPlZ6YCLss4r77jbu6j3sendJoiQ==", - "dependencies": { - "@typescript-eslint/scope-manager": "6.7.3", - "@typescript-eslint/types": "6.7.3", - "@typescript-eslint/typescript-estree": "6.7.3", - "@typescript-eslint/visitor-keys": "6.7.3", + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.9.0.tgz", + "integrity": "sha512-GZmjMh4AJ/5gaH4XF2eXA8tMnHWP+Pm1mjQR2QN4Iz+j/zO04b9TOvJYOX2sCNIQHtRStKTxRY1FX7LhpJT4Gw==", + "dependencies": { + "@typescript-eslint/scope-manager": "6.9.0", + "@typescript-eslint/types": "6.9.0", + "@typescript-eslint/typescript-estree": "6.9.0", + "@typescript-eslint/visitor-keys": "6.9.0", "debug": "^4.3.4" }, "engines": { @@ -9884,12 +9884,12 @@ } }, "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/scope-manager": { - "version": "6.7.3", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.7.3.tgz", - "integrity": "sha512-wOlo0QnEou9cHO2TdkJmzF7DFGvAKEnB82PuPNHpT8ZKKaZu6Bm63ugOTn9fXNJtvuDPanBc78lGUGGytJoVzQ==", + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.9.0.tgz", + "integrity": "sha512-1R8A9Mc39n4pCCz9o79qRO31HGNDvC7UhPhv26TovDsWPBDx+Sg3rOZdCELIA3ZmNoWAuxaMOT7aWtGRSYkQxw==", "dependencies": { - "@typescript-eslint/types": "6.7.3", - "@typescript-eslint/visitor-keys": "6.7.3" + "@typescript-eslint/types": "6.9.0", + "@typescript-eslint/visitor-keys": "6.9.0" }, "engines": { "node": "^16.0.0 || >=18.0.0" @@ -9900,9 +9900,9 @@ } }, "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/types": { - "version": "6.7.3", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.7.3.tgz", - "integrity": "sha512-4g+de6roB2NFcfkZb439tigpAMnvEIg3rIjWQ+EM7IBaYt/CdJt6em9BJ4h4UpdgaBWdmx2iWsafHTrqmgIPNw==", + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.9.0.tgz", + "integrity": "sha512-+KB0lbkpxBkBSiVCuQvduqMJy+I1FyDbdwSpM3IoBS7APl4Bu15lStPjgBIdykdRqQNYqYNMa8Kuidax6phaEw==", "engines": { "node": "^16.0.0 || >=18.0.0" }, @@ -9912,12 +9912,12 @@ } }, "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree": { - "version": "6.7.3", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.7.3.tgz", - "integrity": "sha512-YLQ3tJoS4VxLFYHTw21oe1/vIZPRqAO91z6Uv0Ss2BKm/Ag7/RVQBcXTGcXhgJMdA4U+HrKuY5gWlJlvoaKZ5g==", + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.9.0.tgz", + "integrity": "sha512-NJM2BnJFZBEAbCfBP00zONKXvMqihZCrmwCaik0UhLr0vAgb6oguXxLX1k00oQyD+vZZ+CJn3kocvv2yxm4awQ==", "dependencies": { - "@typescript-eslint/types": "6.7.3", - "@typescript-eslint/visitor-keys": "6.7.3", + "@typescript-eslint/types": "6.9.0", + "@typescript-eslint/visitor-keys": "6.9.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -9938,11 +9938,11 @@ } }, "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/visitor-keys": { - "version": "6.7.3", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.7.3.tgz", - "integrity": "sha512-HEVXkU9IB+nk9o63CeICMHxFWbHWr3E1mpilIQBe9+7L/lH97rleFLVtYsfnWB+JVMaiFnEaxvknvmIzX+CqVg==", + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.9.0.tgz", + "integrity": "sha512-dGtAfqjV6RFOtIP8I0B4ZTBRrlTT8NHHlZZSchQx3qReaoDeXhYM++M4So2AgFK9ZB0emRPA6JI1HkafzA2Ibg==", "dependencies": { - "@typescript-eslint/types": "6.7.3", + "@typescript-eslint/types": "6.9.0", "eslint-visitor-keys": "^3.4.1" }, "engines": { @@ -17059,9 +17059,9 @@ } }, "node_modules/eslint-config-prettier": { - "version": "8.10.0", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.10.0.tgz", - "integrity": "sha512-SM8AMJdeQqRYT9O9zguiruQZaN7+z+E4eAP9oiLNGKMtomwaB1E9dcgUD6ZAn/eQAb52USbvezbiljfZUhbJcg==", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.0.0.tgz", + "integrity": "sha512-IcJsTkJae2S35pRsRAwoCE+925rJJStOdkKnLVgtE+tEpqU0EVVM7OqrwxqgptKdX29NUwC82I5pXsGFIgSevw==", "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -17247,25 +17247,25 @@ } }, "node_modules/eslint-plugin-import": { - "version": "2.28.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.28.1.tgz", - "integrity": "sha512-9I9hFlITvOV55alzoKBI+K9q74kv0iKMeY6av5+umsNwayt59fz692daGyjR+oStBQgx6nwR9rXldDev3Clw+A==", - "dependencies": { - "array-includes": "^3.1.6", - "array.prototype.findlastindex": "^1.2.2", - "array.prototype.flat": "^1.3.1", - "array.prototype.flatmap": "^1.3.1", + "version": "2.29.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.0.tgz", + "integrity": "sha512-QPOO5NO6Odv5lpoTkddtutccQjysJuFxoPS7fAHO+9m9udNHvTCPSAMW9zGAYj8lAIdr40I8yPCdUYrncXtrwg==", + "dependencies": { + "array-includes": "^3.1.7", + "array.prototype.findlastindex": "^1.2.3", + "array.prototype.flat": "^1.3.2", + "array.prototype.flatmap": "^1.3.2", "debug": "^3.2.7", "doctrine": "^2.1.0", - "eslint-import-resolver-node": "^0.3.7", + "eslint-import-resolver-node": "^0.3.9", "eslint-module-utils": "^2.8.0", - "has": "^1.0.3", - "is-core-module": "^2.13.0", + "hasown": "^2.0.0", + "is-core-module": "^2.13.1", "is-glob": "^4.0.3", "minimatch": "^3.1.2", - "object.fromentries": "^2.0.6", - "object.groupby": "^1.0.0", - "object.values": "^1.1.6", + "object.fromentries": "^2.0.7", + "object.groupby": "^1.0.1", + "object.values": "^1.1.7", "semver": "^6.3.1", "tsconfig-paths": "^3.14.2" }, @@ -19186,9 +19186,12 @@ } }, "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/function.prototype.name": { "version": "1.1.6", @@ -20154,6 +20157,17 @@ "minimalistic-assert": "^1.0.1" } }, + "node_modules/hasown": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", + "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/hast-to-hyperscript": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/hast-to-hyperscript/-/hast-to-hyperscript-9.0.1.tgz", @@ -21191,11 +21205,11 @@ } }, "node_modules/is-core-module": { - "version": "2.13.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.0.tgz", - "integrity": "sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==", + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", "dependencies": { - "has": "^1.0.3" + "hasown": "^2.0.0" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -36958,20 +36972,20 @@ "@emotion/eslint-plugin": "^11.11.0", "@nx/eslint-plugin": "^16.5.0", "@tablecheck/eslint-plugin": "^4.0.0", - "@typescript-eslint/eslint-plugin": "^6.2.0", - "@typescript-eslint/parser": "^6.2.0", + "@typescript-eslint/eslint-plugin": "^6.9.0", + "@typescript-eslint/parser": "^6.9.0", "eslint-config-airbnb": "^19.0.4", "eslint-config-airbnb-typescript": "^17.1.0", - "eslint-config-prettier": "^8.8.0", + "eslint-config-prettier": "^9.0.0", "eslint-formatter-pretty": "^5.0.0", "eslint-import-resolver-babel-module": "^5.3.2", - "eslint-import-resolver-typescript": "^3.5.5", - "eslint-plugin-cypress": "^2.13.3", + "eslint-import-resolver-typescript": "^3.6.1", + "eslint-plugin-cypress": "^2.15.1", "eslint-plugin-eslint-comments": "^3.2.0", - "eslint-plugin-import": "^2.27.5", + "eslint-plugin-import": "^2.29.0", "eslint-plugin-jsx-a11y": "^6.7.1", "eslint-plugin-promise": "^6.1.1", - "eslint-plugin-react": "^7.33.0", + "eslint-plugin-react": "^7.33.2", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.4.3", "fs-extra": "^11.1.1", diff --git a/packages/eslint-config/package.json b/packages/eslint-config/package.json index 14d06475..6d5476e3 100644 --- a/packages/eslint-config/package.json +++ b/packages/eslint-config/package.json @@ -28,20 +28,20 @@ "@emotion/eslint-plugin": "^11.11.0", "@nx/eslint-plugin": "^16.5.0", "@tablecheck/eslint-plugin": "^4.0.0", - "@typescript-eslint/eslint-plugin": "^6.2.0", - "@typescript-eslint/parser": "^6.2.0", + "@typescript-eslint/eslint-plugin": "^6.9.0", + "@typescript-eslint/parser": "^6.9.0", "eslint-config-airbnb": "^19.0.4", "eslint-config-airbnb-typescript": "^17.1.0", - "eslint-config-prettier": "^8.8.0", + "eslint-config-prettier": "^9.0.0", "eslint-formatter-pretty": "^5.0.0", "eslint-import-resolver-babel-module": "^5.3.2", - "eslint-import-resolver-typescript": "^3.5.5", - "eslint-plugin-cypress": "^2.13.3", + "eslint-import-resolver-typescript": "^3.6.1", + "eslint-plugin-cypress": "^2.15.1", "eslint-plugin-eslint-comments": "^3.2.0", - "eslint-plugin-import": "^2.27.5", + "eslint-plugin-import": "^2.29.0", "eslint-plugin-jsx-a11y": "^6.7.1", "eslint-plugin-promise": "^6.1.1", - "eslint-plugin-react": "^7.33.0", + "eslint-plugin-react": "^7.33.2", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.4.3", "fs-extra": "^11.1.1", diff --git a/packages/eslint-config/src/overrides/documentation.ts b/packages/eslint-config/src/overrides/documentation.ts index a0c70828..ce5ba845 100644 --- a/packages/eslint-config/src/overrides/documentation.ts +++ b/packages/eslint-config/src/overrides/documentation.ts @@ -14,5 +14,6 @@ export const documentationOverrides: Linter.ConfigOverride = { 'react-hooks/exhaustive-deps': 'off', 'react/function-component-definition': 'off', 'react/jsx-no-constructed-context-values': 'off', + 'react-refresh/only-export-components': 'off', }, }; diff --git a/packages/eslint-config/src/overrides/tests.ts b/packages/eslint-config/src/overrides/tests.ts index 68efdedf..60355994 100644 --- a/packages/eslint-config/src/overrides/tests.ts +++ b/packages/eslint-config/src/overrides/tests.ts @@ -19,6 +19,7 @@ export const testOverrides: Linter.ConfigOverride = { 'prefer-promise-reject-errors': 'off', 'global-require': 'off', 'react/prop-types': 'off', + 'react-refresh/only-export-components': 'off', 'jsx-a11y/accessible-emoji': 'off', 'jsx-a11y/alt-text': 'off', diff --git a/packages/eslint-config/src/overrides/typescriptTests.ts b/packages/eslint-config/src/overrides/typescriptTests.ts index fe4bbda7..c19f6849 100644 --- a/packages/eslint-config/src/overrides/typescriptTests.ts +++ b/packages/eslint-config/src/overrides/typescriptTests.ts @@ -20,6 +20,8 @@ export const typescriptTestsOverrides = typescriptOverrides // here we use the more lenient consistent-return to help prevent weird errors '@typescript-eslint/explicit-module-boundary-types': 'off', 'consistent-return': 'error', + '@typescript-eslint/unbound-method': 'off', + '@typescript-eslint/no-unsafe-assignment': 'off', }, env: { node: true, diff --git a/packages/eslint-config/src/rules/namingConvention.ts b/packages/eslint-config/src/rules/namingConvention.ts index d55db236..943c4e5e 100644 --- a/packages/eslint-config/src/rules/namingConvention.ts +++ b/packages/eslint-config/src/rules/namingConvention.ts @@ -66,6 +66,10 @@ export const namingRules: Linter.RulesRecord = { types: ['function'], format: ['PascalCase', 'camelCase'], }, + { + selector: 'import', + format: ['PascalCase', 'camelCase', 'UPPER_CASE'], + }, { selector: ['function', 'classMethod', 'objectLiteralMethod'], format: ['PascalCase', 'camelCase'], diff --git a/packages/eslint-config/src/rules/react.ts b/packages/eslint-config/src/rules/react.ts index 1a416477..1cf2469a 100644 --- a/packages/eslint-config/src/rules/react.ts +++ b/packages/eslint-config/src/rules/react.ts @@ -55,7 +55,7 @@ export const reactRules: Linter.RulesRecord = { 'react/jsx-no-useless-fragment': ['error', { allowExpressions: true }], '@tablecheck/consistent-react-import': 'error', 'react-refresh/only-export-components': [ - 'error', + 'warn', { allowConstantExport: true }, ], }; diff --git a/packages/eslint-plugin/__tests__/consistentReactImport.test.ts b/packages/eslint-plugin/__tests__/consistentReactImport.test.ts index 71661fb7..53dabbe5 100644 --- a/packages/eslint-plugin/__tests__/consistentReactImport.test.ts +++ b/packages/eslint-plugin/__tests__/consistentReactImport.test.ts @@ -8,13 +8,13 @@ import { const ruleTester = new RuleTester({ parser: '@typescript-eslint/parser', parserOptions: { - project: './tsconfig.test.json', + project: './fixtures/tsconfig.test.json', tsconfigRootDir: __dirname, sourceType: 'module', }, }); -const filename = './test_src/default.tsx'; +const filename = './fixtures/test_src/default.tsx'; const invalidTests = [ { code: `import { useState } from 'react';const [val, setVal] = useState(true);`, diff --git a/packages/eslint-plugin/__tests__/test_src/.env b/packages/eslint-plugin/__tests__/fixtures/test_src/.env similarity index 100% rename from packages/eslint-plugin/__tests__/test_src/.env rename to packages/eslint-plugin/__tests__/fixtures/test_src/.env diff --git a/packages/eslint-plugin/__tests__/test_src/default.tsx b/packages/eslint-plugin/__tests__/fixtures/test_src/default.tsx similarity index 100% rename from packages/eslint-plugin/__tests__/test_src/default.tsx rename to packages/eslint-plugin/__tests__/fixtures/test_src/default.tsx diff --git a/packages/eslint-plugin/__tests__/test_src/feature1/index.ts b/packages/eslint-plugin/__tests__/fixtures/test_src/feature1/index.ts similarity index 100% rename from packages/eslint-plugin/__tests__/test_src/feature1/index.ts rename to packages/eslint-plugin/__tests__/fixtures/test_src/feature1/index.ts diff --git a/packages/eslint-plugin/__tests__/test_src/feature1/slice1/index.ts b/packages/eslint-plugin/__tests__/fixtures/test_src/feature1/slice1/index.ts similarity index 100% rename from packages/eslint-plugin/__tests__/test_src/feature1/slice1/index.ts rename to packages/eslint-plugin/__tests__/fixtures/test_src/feature1/slice1/index.ts diff --git a/packages/eslint-plugin/__tests__/test_src/feature1/slice1/inner1/index.ts b/packages/eslint-plugin/__tests__/fixtures/test_src/feature1/slice1/inner1/index.ts similarity index 100% rename from packages/eslint-plugin/__tests__/test_src/feature1/slice1/inner1/index.ts rename to packages/eslint-plugin/__tests__/fixtures/test_src/feature1/slice1/inner1/index.ts diff --git a/packages/eslint-plugin/__tests__/test_src/feature1/slice1/inner2/index.ts b/packages/eslint-plugin/__tests__/fixtures/test_src/feature1/slice1/inner2/index.ts similarity index 100% rename from packages/eslint-plugin/__tests__/test_src/feature1/slice1/inner2/index.ts rename to packages/eslint-plugin/__tests__/fixtures/test_src/feature1/slice1/inner2/index.ts diff --git a/packages/eslint-plugin/__tests__/test_src/feature1/slice1/second.ts b/packages/eslint-plugin/__tests__/fixtures/test_src/feature1/slice1/second.ts similarity index 100% rename from packages/eslint-plugin/__tests__/test_src/feature1/slice1/second.ts rename to packages/eslint-plugin/__tests__/fixtures/test_src/feature1/slice1/second.ts diff --git a/packages/eslint-plugin/__tests__/test_src/feature1/slice2/index.ts b/packages/eslint-plugin/__tests__/fixtures/test_src/feature1/slice2/index.ts similarity index 100% rename from packages/eslint-plugin/__tests__/test_src/feature1/slice2/index.ts rename to packages/eslint-plugin/__tests__/fixtures/test_src/feature1/slice2/index.ts diff --git a/packages/eslint-plugin/__tests__/test_src/feature1/slice2/second.ts b/packages/eslint-plugin/__tests__/fixtures/test_src/feature1/slice2/second.ts similarity index 100% rename from packages/eslint-plugin/__tests__/test_src/feature1/slice2/second.ts rename to packages/eslint-plugin/__tests__/fixtures/test_src/feature1/slice2/second.ts diff --git a/packages/eslint-plugin/__tests__/test_src/feature2/index.ts b/packages/eslint-plugin/__tests__/fixtures/test_src/feature2/index.ts similarity index 100% rename from packages/eslint-plugin/__tests__/test_src/feature2/index.ts rename to packages/eslint-plugin/__tests__/fixtures/test_src/feature2/index.ts diff --git a/packages/eslint-plugin/__tests__/test_src/feature2/sliceA/index.ts b/packages/eslint-plugin/__tests__/fixtures/test_src/feature2/sliceA/index.ts similarity index 100% rename from packages/eslint-plugin/__tests__/test_src/feature2/sliceA/index.ts rename to packages/eslint-plugin/__tests__/fixtures/test_src/feature2/sliceA/index.ts diff --git a/packages/eslint-plugin/__tests__/fixtures/test_src/tsconfig.test_root.json b/packages/eslint-plugin/__tests__/fixtures/test_src/tsconfig.test_root.json new file mode 100644 index 00000000..32e4deeb --- /dev/null +++ b/packages/eslint-plugin/__tests__/fixtures/test_src/tsconfig.test_root.json @@ -0,0 +1,12 @@ +{ + "extends": "../../../../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "../", + "baseUrl": ".", + "paths": { + "~/*": ["./*"] + } + }, + "include": ["**/*.ts", "*.tsx"], + "exclude": ["node_modules"] +} diff --git a/packages/eslint-plugin/__tests__/fixtures/tsconfig.base_only.json b/packages/eslint-plugin/__tests__/fixtures/tsconfig.base_only.json new file mode 100644 index 00000000..b484f732 --- /dev/null +++ b/packages/eslint-plugin/__tests__/fixtures/tsconfig.base_only.json @@ -0,0 +1,9 @@ +{ + "extends": "../../../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "./test_src", + "baseUrl": "./test_src" + }, + "include": ["test_src/**/*.ts", "test_src/*.tsx"], + "exclude": ["node_modules"] +} diff --git a/packages/eslint-plugin/__tests__/tsconfig.test.json b/packages/eslint-plugin/__tests__/fixtures/tsconfig.test.json similarity index 82% rename from packages/eslint-plugin/__tests__/tsconfig.test.json rename to packages/eslint-plugin/__tests__/fixtures/tsconfig.test.json index a194771a..2a1c451c 100644 --- a/packages/eslint-plugin/__tests__/tsconfig.test.json +++ b/packages/eslint-plugin/__tests__/fixtures/tsconfig.test.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.base.json", + "extends": "../../../../tsconfig.base.json", "compilerOptions": { "rootDir": "./test_src", "baseUrl": "./test_src", diff --git a/packages/eslint-plugin/__tests__/fixtures/tsconfig.test_extends.json b/packages/eslint-plugin/__tests__/fixtures/tsconfig.test_extends.json new file mode 100644 index 00000000..7be543a8 --- /dev/null +++ b/packages/eslint-plugin/__tests__/fixtures/tsconfig.test_extends.json @@ -0,0 +1,12 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + "rootDir": "./test_src", + "baseUrl": "./test_src", + "paths": { + "~/*": ["./test_src/*"] + } + }, + "include": ["test_src/**/*.ts", "test_src/*.tsx"], + "exclude": ["node_modules"] +} diff --git a/packages/eslint-plugin/__tests__/forbiddenImports.test.ts b/packages/eslint-plugin/__tests__/forbiddenImports.test.ts index 07476245..49a7a6e9 100644 --- a/packages/eslint-plugin/__tests__/forbiddenImports.test.ts +++ b/packages/eslint-plugin/__tests__/forbiddenImports.test.ts @@ -6,13 +6,13 @@ import { forbiddenImports as rule, messageId } from '../src/forbiddenImports'; const ruleTester = new RuleTester({ parser: '@typescript-eslint/parser', parserOptions: { - project: './tsconfig.test.json', + project: './fixtures/tsconfig.test.json', tsconfigRootDir: __dirname, sourceType: 'module', }, }); -const filename = './test_src/default.tsx'; +const filename = './fixtures/test_src/default.tsx'; ruleTester.run('forbiddenImports > valid other import formats', rule, { valid: [ diff --git a/packages/eslint-plugin/__tests__/shortestImport.test.ts b/packages/eslint-plugin/__tests__/shortestImport.test.ts index e40c8008..96335c52 100644 --- a/packages/eslint-plugin/__tests__/shortestImport.test.ts +++ b/packages/eslint-plugin/__tests__/shortestImport.test.ts @@ -1,15 +1,31 @@ +import { join as pathJoin } from 'path'; + import { RuleTester } from '@typescript-eslint/rule-tester'; import { shortestImport as rule, messageId } from '../src/shortestImport'; -const ruleTester = new RuleTester({ - parser: '@typescript-eslint/parser', - parserOptions: { +const typescriptSetups = [ + { + name: 'basic', project: './tsconfig.test.json', + tsconfigRootDir: pathJoin(__dirname, 'fixtures'), + }, + { + name: 'baseUrl Only', + project: './tsconfig.base_only.json', + tsconfigRootDir: pathJoin(__dirname, 'fixtures'), + }, + { + name: 'root', + project: './test_src/tsconfig.test_root.json', + tsconfigRootDir: pathJoin(__dirname, 'fixtures'), + }, + { + name: 'extends', + project: './fixtures/tsconfig.test_extends.json', tsconfigRootDir: __dirname, - sourceType: 'module', }, -}); +] as const; type TestResult = (Omit & @@ -17,7 +33,12 @@ type TestResult = ? { code: string } : { code: string; output: string }))[]; -function buildCodeCase({ +function buildCodeCase< + T extends { + name?: string; + skipConfigs?: (typeof typescriptSetups)[number]['name'][]; + }, +>({ path, fixedPath, importType, @@ -76,117 +97,177 @@ function convertPathCaseToCodeCase< }, [] as TestResult); } -ruleTester.run('shortestImport', rule, { - valid: convertPathCaseToCodeCase([ - { - path: './second', - filename: './test_src/feature1/slice1/index.ts', - }, - { - path: '../slice2', - filename: './test_src/feature1/slice1/index.ts', - }, - { - path: '../inner2', - filename: './test_src/feature1/slice1/inner1/index.ts', - }, - { - path: '~/feature1/slice2/second', - filename: './test_src/feature1/slice1/inner1/index.ts', - }, - { - path: '~/feature2/index', - filename: './test_src/feature1/slice1/index.ts', - }, - { - path: '~/feature1/index', - filename: './test_src/feature1/slice1/index.ts', - options: [['~/feature1']], - }, - { - path: '@node/module', - filename: './test_src/feature1/slice1/inner1/index.ts', - }, - { - path: 'react', - filename: './test_src/feature1/slice1/inner1/index.ts', - }, - { - path: '.', - filename: './test_src/feature1/slice1/inner1/index.ts', - }, - ]), - invalid: convertPathCaseToCodeCase([ - { - name: 'prefer relative path over alias path 1', - path: '~/feature1/slice1/second', - fixedPath: './second', - filename: './test_src/feature1/slice1/index.ts', - errors: [{ messageId }], - }, - { - name: 'prefer relative path over alias path 2', - path: '~/feature1/slice1/second', - fixedPath: './slice1/second', - filename: './test_src/feature1/index.ts', - errors: [{ messageId }], - }, - { - name: 'prefer alias path over parent through baseUrl', - path: '../../feature2', - fixedPath: '~/feature2', - filename: './test_src/feature1/slice1/index.ts', - errors: [{ messageId }], - }, - { - name: 'prefer alias path over parent through baseUrl child', - path: '../../feature1/index', - fixedPath: '~/feature1/index', - filename: './test_src/feature1/slice1/index.ts', - errors: [{ messageId }], - }, - { - name: 'prefer alias path over baseUrl resolve', - path: 'feature2', - fixedPath: '~/feature2', - filename: './test_src/feature1/slice1/index.ts', - errors: [{ messageId }], - }, - { - name: 'prefer relative parent path over alias/baseUrl', - path: 'feature1/slice2', - fixedPath: '../slice2', - filename: './test_src/feature1/slice1/index.ts', - errors: [{ messageId }], - }, - { - name: 'prefer alias over baseUrl and relative through root', - path: '../../feature2/sliceA', - fixedPath: '~/feature2/sliceA', - filename: './test_src/feature1/slice1/index.ts', - errors: [{ messageId }], - }, - { - name: 'prefer alias over deep relative parent (equal length)', - path: '../../slice2/second', - fixedPath: '~/feature1/slice2/second', - filename: './test_src/feature1/slice1/inner1/index.ts', - errors: [{ messageId }], - }, - { - name: 'prefer relative parent over short alias', - path: '~/feature1/index', - fixedPath: '../index', - filename: './test_src/feature1/slice1/index.ts', - errors: [{ messageId }], - }, - { - name: 'use alias over short/equal relative when option set', - path: '../index', - fixedPath: '~/feature1/index', - filename: './test_src/feature1/slice1/index.ts', - errors: [{ messageId }], - options: [['~/feature1']], +function mapConfig< + T extends { + filename: string; + path: string; + fixedPath?: string; + skipConfigs?: unknown; + }, +>(configName: (typeof typescriptSetups)[number]['name']) { + return (x: T) => { + const { + filename, + path: configPath, + fixedPath: configFixedPath, + skipConfigs, + ...rest + } = x; + const folderPrefixes = [] as string[]; + if (configName === 'extends') { + folderPrefixes.push('fixtures'); + } + let correctedPath = configPath; + let correctedFixedPath = configFixedPath; + if (configName === 'baseUrl Only') { + if (configPath.startsWith('~/')) { + correctedPath = configPath.substring(2); + } + if (configFixedPath?.startsWith('~/')) { + correctedFixedPath = configFixedPath.substring(2); + } + } + return { + ...rest, + path: correctedPath, + fixedPath: correctedFixedPath, + filename: pathJoin(...folderPrefixes, filename), + }; + }; +} + +typescriptSetups.forEach((config) => { + const ruleTester = new RuleTester({ + parser: '@typescript-eslint/parser', + parserOptions: { + project: config.project, + tsconfigRootDir: config.tsconfigRootDir, + sourceType: 'module', }, - ]), + }); + ruleTester.run(`shortestImport [${config.name}]`, rule, { + valid: convertPathCaseToCodeCase( + [ + { + path: './second', + filename: './test_src/feature1/slice1/index.ts', + }, + { + path: './second', + filename: './test_src/feature1/index.ts', + }, + { + path: '../slice2', + filename: './test_src/feature1/slice1/index.ts', + }, + { + path: '../inner2', + filename: './test_src/feature1/slice1/inner1/index.ts', + }, + { + path: '~/feature1/slice2/second', + filename: './test_src/feature1/slice1/inner1/index.ts', + }, + { + path: '~/feature2/index', + filename: './test_src/feature1/slice1/index.ts', + }, + { + path: '~/feature1/index', + filename: './test_src/feature1/slice1/index.ts', + options: [['~/feature1', 'feature1']], + }, + { + path: '@node/module', + filename: './test_src/feature1/slice1/inner1/index.ts', + }, + { + path: 'react', + filename: './test_src/feature1/slice1/inner1/index.ts', + }, + { + path: '.', + filename: './test_src/feature1/slice1/inner1/index.ts', + }, + ].map(mapConfig(config.name)), + ), + invalid: convertPathCaseToCodeCase( + [ + { + name: 'prefer relative path over alias path 1', + path: '~/feature1/slice1/second', + fixedPath: './second', + filename: './test_src/feature1/slice1/index.ts', + errors: [{ messageId }], + }, + { + name: 'prefer relative path over alias path 2', + path: '~/feature1/slice1/second', + fixedPath: './slice1/second', + filename: './test_src/feature1/index.ts', + errors: [{ messageId }], + }, + { + name: 'prefer alias path over parent through baseUrl', + path: '../../feature2', + fixedPath: '~/feature2', + filename: './test_src/feature1/slice1/index.ts', + errors: [{ messageId }], + }, + { + name: 'prefer alias path over parent through baseUrl child', + path: '../../feature1/index', + fixedPath: '~/feature1/index', + filename: './test_src/feature1/slice1/index.ts', + errors: [{ messageId }], + }, + { + skipConfigs: ['baseUrl Only'], + name: 'prefer alias path over baseUrl resolve', + path: 'feature2', + fixedPath: '~/feature2', + filename: './test_src/feature1/slice1/index.ts', + errors: [{ messageId }], + }, + { + name: 'prefer relative parent path over alias/baseUrl', + path: 'feature1/slice2', + fixedPath: '../slice2', + filename: './test_src/feature1/slice1/index.ts', + errors: [{ messageId }], + }, + { + name: 'prefer alias over baseUrl and relative through root', + path: '../../feature2/sliceA', + fixedPath: '~/feature2/sliceA', + filename: './test_src/feature1/slice1/index.ts', + errors: [{ messageId }], + }, + { + name: 'prefer alias over deep relative parent (equal length)', + path: '../../slice2/second', + fixedPath: '~/feature1/slice2/second', + filename: './test_src/feature1/slice1/inner1/index.ts', + errors: [{ messageId }], + }, + { + name: 'prefer relative parent over short alias', + path: '~/feature1/index', + fixedPath: '../index', + filename: './test_src/feature1/slice1/index.ts', + errors: [{ messageId }], + }, + { + name: 'use alias over short/equal relative when option set', + path: '../index', + fixedPath: '~/feature1/index', + filename: './test_src/feature1/slice1/index.ts', + errors: [{ messageId }], + options: [['~/feature1', 'feature1']], + }, + ] + .filter((c) => !c.skipConfigs || !c.skipConfigs.includes(config.name)) + .map(mapConfig(config.name)), + ), + }); }); diff --git a/packages/eslint-plugin/__tests__/tsconfig.base.json b/packages/eslint-plugin/__tests__/tsconfig.base.json new file mode 100644 index 00000000..804f1775 --- /dev/null +++ b/packages/eslint-plugin/__tests__/tsconfig.base.json @@ -0,0 +1,8 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "baseUrl": "." + }, + "include": [], + "exclude": ["node_modules"] +} diff --git a/packages/eslint-plugin/src/shortestImport.ts b/packages/eslint-plugin/src/shortestImport.ts index 2afb4735..73a87573 100644 --- a/packages/eslint-plugin/src/shortestImport.ts +++ b/packages/eslint-plugin/src/shortestImport.ts @@ -25,7 +25,7 @@ class RuleChecker { private pathMappings: Record; - private aliasPathMappings: Record; + private allPaths: Record; get relativeBaseUrl(): string { return path.relative(this.pathsBasePath as string, this.baseUrl ?? ''); @@ -33,22 +33,23 @@ class RuleChecker { constructor(compilerOptions: CompilerOptions) { const { baseUrl, pathsBasePath, rootDir, rootDirs } = compilerOptions; + this.baseUrl = baseUrl; this.pathsBasePath = pathsBasePath; this.rootDir = rootDir; this.rootDirs = rootDirs; this.compilerPaths = this.composeCompilerPaths(compilerOptions.paths); this.pathMappings = this.composePathMappings(); - this.aliasPathMappings = this.composeAliasPathMappings(); + this.allPaths = { ...this.compilerPaths, ...this.pathMappings }; } private composeCompilerPaths(compilerPaths: CompilerOptions['paths']) { return Object.entries(compilerPaths ?? {}).reduce( (compilerPathsMap, [key, [value]]) => ({ ...compilerPathsMap, - [key.replace(/\/\*$/gi, '')]: value - .replace(/\/\*$/gi, '') - .replace(/^\.\//gi, ''), + [key.replace(/\*$/gi, '')]: this.relativeToBaseUrl( + value.replace(/\/\*$/gi, '').replace(/^\.\//gi, ''), + ), }), {}, ); @@ -56,10 +57,9 @@ class RuleChecker { private composePathMappings() { return Object.fromEntries( - Object.entries({ - ...this.compilerPaths, - ...this.composeBaseUrlPaths(), - } as Record).filter(([key]) => !!key.trim()), + Object.entries(this.composeBaseUrlPaths()).filter( + ([key]) => !!key.trim(), + ), ); } @@ -74,31 +74,23 @@ class RuleChecker { if (dirrent.isDirectory()) return { ...directoryMap, - [dirrent.name]: path.join(this.relativeBaseUrl, dirrent.name), + [`${dirrent.name}/`]: this.relativeToBaseUrl( + path.join(this.relativeBaseUrl, dirrent.name), + ), }; return { ...directoryMap, - [dirrent.name.replace(/\.[^.]+$/gi, '')]: path - .join(this.relativeBaseUrl, dirrent.name) - .replace(/^\.\//gi, ''), + [dirrent.name.replace(/\.[^.]+$/gi, '')]: this.relativeToBaseUrl( + path + .join(this.relativeBaseUrl, dirrent.name) + .replace(/^\.\//gi, ''), + ), }; }, {} as Record, ); } - private composeAliasPathMappings() { - return this.doesCompilerPathsIncludeBaseUrl() - ? this.compilerPaths - : this.pathMappings; - } - - private doesCompilerPathsIncludeBaseUrl() { - return Object.values(this.compilerPaths).some((value) => - value.startsWith(this.relativeBaseUrl), - ); - } - private getImportMeta( context: Readonly< TSESLint.RuleContext< @@ -140,20 +132,30 @@ class RuleChecker { ) { const { importPath, resolvedImportPath, resolvedFilePath } = this.getImportMeta(context, node); + if (!importPath) return; const relativePath = this.getRelativeImport({ importPath, resolvedImportPath, resolvedFilePath, }); - const aliasPaths = this.getPathAliasImports({ + const aliasPaths = this.getPathImports({ + mappings: this.compilerPaths, + resolvedImportPath, + importPath, + resolvedFilePath, + }); + const baseUrlPaths = this.getPathImports({ + mappings: this.pathMappings, resolvedImportPath, + importPath, resolvedFilePath, }); const preferredPath = this.getPreferredPath({ resolvedFilePath, relativePath, aliasPaths, + baseUrlPaths, avoidRelativeParents: context.options[0] || [], }); @@ -174,21 +176,36 @@ class RuleChecker { private shouldNotChangeImport(importPath: string) { if (importPath.startsWith('@') || importPath === '.') return true; - const isPathMapping = Object.keys(this.pathMappings).some((key) => - importPath.startsWith(key), + const isPathMapping = Object.keys(this.allPaths).some( + (key) => + importPath.startsWith(key) || + importPath.startsWith(key.replace(/\/$/, '')), ); + if (isPathMapping) return false; return !importPath.startsWith('.') && !importPath.startsWith('/'); } private resolveImport(importPath: string) { - const importParts = importPath.split('/'); - if (this.pathMappings[importParts[0]]) { - return [this.pathMappings[importParts[0]]] - .concat(importParts.slice(1)) - .join('/'); + const matchedPathMappingKey = Object.keys(this.allPaths).find( + (key) => importPath.startsWith(key) || `${importPath}/`.startsWith(key), + ); + + if (matchedPathMappingKey) { + const base = this.allPaths[matchedPathMappingKey]; + const append = importPath + .replace(matchedPathMappingKey.replace(/\/$/, ''), '') + .replace(matchedPathMappingKey, ''); + + return `./${path.join(base, append)}`; } - return importParts.join('/'); + return importPath; + } + + private relativeToBaseUrl(filepath: string) { + if (!this.baseUrl) return filepath; + const forcedImportPath = this.forceAppend(this.baseUrl, filepath); + return `./${path.relative(this.baseUrl, forcedImportPath)}`; } private getRelativeImport({ @@ -201,55 +218,71 @@ class RuleChecker { resolvedFilePath: string; }) { if (importPath.startsWith('.')) return importPath; + const relativePath = path.relative( path.dirname(resolvedFilePath), - resolvedImportPath, + this.relativeToBaseUrl(resolvedImportPath), ); + if (relativePath.startsWith('.')) return relativePath; return `./${relativePath}`; } - private getPathAliasImports({ - resolvedImportPath: importPath, + private getPathImports({ + mappings, + importPath, + resolvedImportPath, resolvedFilePath, }: { + mappings: Record; + importPath: string; resolvedImportPath: string; resolvedFilePath: string; }) { - let resolvedImportPath = importPath; - if (importPath.startsWith('.')) { - resolvedImportPath = path.resolve( - path.dirname(resolvedFilePath), - importPath, - ); - } - const matchedMappings = Object.entries(this.aliasPathMappings).filter( - ([, value]) => resolvedImportPath.includes(value), - ); - return matchedMappings.map(([key, value]) => - resolvedImportPath.replace( - new RegExp(`^.*?${value.replace(/\//gi, '\\/')}`), - key, - ), - ); + const rootRelativeImportPath = importPath.startsWith('.') + ? `./${path.relative( + process.cwd(), + path.resolve(path.dirname(resolvedFilePath), importPath), + )}` + : resolvedImportPath; + return Object.entries(mappings) + .map(([key, value]) => { + const keyRegexp = new RegExp( + `^${key + .replace(/\/$/g, '') + .replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(\\/|$)`, + ); + const hasImportMatch = importPath.match(keyRegexp); + const hasResolvedMatch = `./${rootRelativeImportPath}`.includes(value); + if (!hasImportMatch && !hasResolvedMatch) return undefined; + if (hasImportMatch) return importPath; + return rootRelativeImportPath.replace( + new RegExp(`^.*?${value.replace(/\//gi, '\\/')}`), + key, + ); + }) + .map((a) => a?.replace(/\/+/gi, '/').replace(/\/$/gi, '')) + .filter((a): a is string => !!a); } private getPreferredPath({ resolvedFilePath, relativePath, aliasPaths, + baseUrlPaths, avoidRelativeParents, }: { resolvedFilePath: string; relativePath: string; aliasPaths: string[]; + baseUrlPaths: string[]; avoidRelativeParents: string[]; }) { - if (!aliasPaths.length) return relativePath; + if (!aliasPaths.length && !baseUrlPaths.length) return relativePath; const parentSlugs = relativePath.split('/').filter((s) => s === '..'); const shouldAvoidRelative = this.relativeGoesThroughBaseUrl(relativePath, resolvedFilePath) || - aliasPaths.some((aliasPath) => { + [...aliasPaths, ...baseUrlPaths].some((aliasPath) => { if (!avoidRelativeParents.length) return false; const relativeRoot = aliasPath .split('/') @@ -257,23 +290,46 @@ class RuleChecker { .join('/'); return avoidRelativeParents.includes(relativeRoot); }); - const aliasWithLength = aliasPaths + const allPathsWithLength = (aliasPaths.length ? aliasPaths : baseUrlPaths) .map((aliasPath) => ({ aliasPath, length: aliasPath.split('/').length, })) - .concat( - shouldAvoidRelative - ? [] - : [ - { - aliasPath: relativePath, - length: relativePath.split('/').length, - }, - ], - ) .sort((a, b) => a.length - b.length); - return aliasWithLength[0]?.aliasPath; + + const shortestAliasPath = allPathsWithLength[0]; + if (shouldAvoidRelative) return shortestAliasPath?.aliasPath; + if (!shortestAliasPath) return relativePath; + return this.shouldPreferRelative(relativePath, shortestAliasPath) + ? relativePath + : shortestAliasPath.aliasPath; + } + + private shouldPreferRelative( + relativePath: string, + shortestAliasPath: { aliasPath: string; length: number }, + ): boolean { + const shortestAliasPathLength = shortestAliasPath.length; + const relativePathLength = relativePath.split('/').length; + if (relativePath.startsWith('./')) { + return relativePathLength - 1 < shortestAliasPathLength; + } + if (relativePath.startsWith('../../')) { + if (relativePathLength === shortestAliasPathLength) { + const pathParts = relativePath.split('/'); + let dotsOverPaths = 0; + pathParts.forEach((part) => { + if (part === '..') dotsOverPaths += 1; + else dotsOverPaths -= 1; + }); + return dotsOverPaths > 0; + } + return relativePathLength < shortestAliasPathLength; + } + if (relativePath.startsWith('../')) { + return relativePathLength <= shortestAliasPathLength; + } + return false; } private relativeGoesThroughBaseUrl( @@ -292,25 +348,43 @@ class RuleChecker { path.dirname(resolvedFilePath), parentPath, ); + return resolvedPathRoot === absoluteImportPath; } + private forceAppend(base: string, append: string) { + const baseParts = base.split('/'); + for (let i = 0; i < baseParts.length; i += 1) { + const lastSegments = baseParts.slice(-1 * (i + 1)).join('/'); + if (append.startsWith(lastSegments)) { + return `/${path.join( + ...baseParts.slice(0, -1 * (i + 1)), + append, + )}`.replace(/[/]+/, '/'); + } + } + return path.resolve(base, append); + } + private getResolvedFilePath(filename: string) { if (!filename.startsWith('/')) { - const resolvedPaths = [ - this.pathsBasePath, - this.rootDir, - ...(this.rootDirs ?? []), - ].map((potentialRoot) => { - if (typeof potentialRoot !== 'string') return undefined; - return [potentialRoot, path.resolve(potentialRoot, filename)]; - }); - const match = resolvedPaths.find((tuple) => { + const resolvedRoots = [this.rootDir, ...(this.rootDirs ?? [])].map( + (potentialRoot) => { + if (typeof potentialRoot !== 'string') return undefined; + return [potentialRoot, this.forceAppend(potentialRoot, filename)]; + }, + ); + const match = resolvedRoots.find((tuple) => { if (!tuple) return false; const [, resolvedPath] = tuple; return fs.existsSync(resolvedPath); }); - return match ? path.relative(match[0], match[1]) : filename; + + if (!match) return filename; + if (this.baseUrl) { + return `./${path.relative(this.baseUrl, match[1])}`; + } + return `./${path.relative(match[0], match[1])}`; } // this is because sometimes `baseUrl` is lowercase (eg `/users/someone/...`) diff --git a/packages/nx/src/executors/quality/executor.ts b/packages/nx/src/executors/quality/executor.ts index 7ed108dd..27b731c4 100644 --- a/packages/nx/src/executors/quality/executor.ts +++ b/packages/nx/src/executors/quality/executor.ts @@ -33,6 +33,7 @@ export default async function runExecutor( const lintResult = await lintRun( { ...options, + hasTypeAwareRules: true, noEslintrc: false, format: process.env.CI ? 'junit' : 'stylish', quiet: !!process.env.CI,