Repository: depcheck/depcheck
Pull request #851 (2023-11-20 21:43:34)

feat(tsconfig): support plugins field
## Motivation

Support packages being used as [Typescript language service plugins](https://www.typescriptlang.org/tsconfig#plugins)

## Changes

This PR adds support for parsing Typescript plugins as dependencies. The logic comes from the [Typescript's guide to create a plugin](https://github.com/microsoft/TypeScript/wiki/Writing-a-Language-Service-Plugin)
> To enable this plugin, users will add an entry to the plugins list in their tsconfig.json file:
```json
{
    "compilerOptions": {
	    ...
        "plugins": [{ "name": "sample-ts-plugin" }]
    }
}
```

## Test

Modified the existing `tsconfig.json` to cover the scenarios proposed.

1. `npm run lint` didn't introduce any new problems (there is an existing one in `src/utils/index.js#L27:10)
1. `npm run test` passed all tests
1. `npm run compile && npm run component && npm run depcheck && npm run depcheck-json` worked OK.



Patch:
diff --git a/src/detector/importDeclaration.js b/src/detector/importDeclaration.js
index 65ae91f..d1cde6f 100644
--- a/src/detector/importDeclaration.js
+++ b/src/detector/importDeclaration.js
@@ -5,14 +5,13 @@ export default function detectImportDeclaration(node, deps) {
     return [];
   }
 
-  // TypeScript "import type X from 'foo'" and "import type X from 'foo/bar'"- doesn't need to depend on the
+  // TypeScript "import type X from 'foo'" - doesn't need to depend on the
   // actual module, instead it can rely on `@types/<module>` instead.
-
-  const packageName = node.source.value.split('/')[0];
-  const typesPackageName = `@types/${packageName}`;
-
-  if (node.importKind === 'type' && deps.includes(typesPackageName)) {
-    return [typesPackageName];
+  if (
+    node.importKind === 'type' &&
+    deps.includes(`@types/${node.source.value}`)
+  ) {
+    return [`@types/${node.source.value}`];
   }
 
   return extractInlineWebpack(node.source.value);
diff --git a/src/parser/tsconfig.js b/src/parser/tsconfig.js
index 1a5ec46..9a4f15f 100644
--- a/src/parser/tsconfig.js
+++ b/src/parser/tsconfig.js
@@ -7,6 +7,7 @@ export default function tsconfigParser(filePath, deps) {
   const foundDeps = [];
   // Typescript uses 'jsonc-parser' to parse tsconfig.json, but json5 is a superset of jsonc syntax (JSON + js comments)
   const tsconfigJson = JSON5.parse(content);
+
   const types = tsconfigJson.compilerOptions?.types;
   if (types) {
     types.forEach((pkg) => {
@@ -21,8 +22,17 @@ export default function tsconfigParser(filePath, deps) {
       }
     });
   }
+
   if (tsconfigJson.extends) {
     foundDeps.push(tsconfigJson.extends);
   }
+
+  const plugins = tsconfigJson.compilerOptions?.plugins;
+  if (plugins) {
+    plugins.forEach((plugin) => {
+      foundDeps.push(plugin.name);
+    });
+  }
+
   return foundDeps.map((p) => requirePackageName(p)).filter(Boolean);
 }


Test patch:
diff --git a/test/fake_modules/tsconfig/package.json b/test/fake_modules/tsconfig/package.json
index a387619..2f262ed 100644
--- a/test/fake_modules/tsconfig/package.json
+++ b/test/fake_modules/tsconfig/package.json
@@ -4,6 +4,7 @@
   "devDependencies": {
     "@mybrand/tsconfig": "*",
     "@types/node": "*",
-    "@types/unused": "*"
+    "@types/unused": "*",
+    "typescript-eslint-language-service": "*"
   }
 }
diff --git a/test/fake_modules/tsconfig/tsconfig.json b/test/fake_modules/tsconfig/tsconfig.json
index 6267e91..ecf6c3d 100644
--- a/test/fake_modules/tsconfig/tsconfig.json
+++ b/test/fake_modules/tsconfig/tsconfig.json
@@ -1,5 +1,13 @@
 {
   "compilerOptions": {
+    "plugins": [
+      {
+        "name": "typescript-eslint-language-service"
+      },
+      {
+        "name": "ts-graphql-plugin"
+      }
+    ],
     "types": ["node", "jest"]
   },
   "extends": "@mybrand/tsconfig"
diff --git a/test/fake_modules/typescript/package.json b/test/fake_modules/typescript/package.json
index e9bdfbd..98d7233 100644
--- a/test/fake_modules/typescript/package.json
+++ b/test/fake_modules/typescript/package.json
@@ -4,7 +4,6 @@
     "@types/org__org-pkg": "0.0.1",
     "@types/react": "0.0.1",
     "@types/node": "0.0.1",
-    "@types/another-typeless-module": "0.0.1",
     "@types/typeless-module": "0.0.1",
     "react": "0.0.1",
     "ts-dep-1": "0.0.1",
diff --git a/test/fake_modules/typescript/typeOnly.ts b/test/fake_modules/typescript/typeOnly.ts
index eb1021a..dc0033e 100644
--- a/test/fake_modules/typescript/typeOnly.ts
+++ b/test/fake_modules/typescript/typeOnly.ts
@@ -1,7 +1,5 @@
 import type { Foo } from 'typeless-module';
-import type { Bar } from 'another-typeless-module/nested-declaration-file';
 
-const foo: Foo = { prop: 'here' };
-const bar: Bar = { prop: 'there' };
+const bar: Foo = { prop: 'here' };
 
-export default { foo, bar };
+export default bar;
diff --git a/test/spec.js b/test/spec.js
index 810812e..a3a770c 100755
--- a/test/spec.js
+++ b/test/spec.js
@@ -227,7 +227,6 @@ export default [
         '@types/react': ['component.tsx'],
         '@types/node': ['esnext.ts'],
         '@types/org__org-pkg': ['esnext.ts'],
-        '@types/another-typeless-module': ['typeOnly.ts'],
         '@types/typeless-module': ['typeOnly.ts'],
         '@org/org-pkg': ['esnext.ts'],
         'ts-dep-1': ['index.ts'],
@@ -239,7 +238,7 @@ export default [
     expectedErrorCode: -1,
   },
   {
-    name: 'support tsconfig extends and types fields',
+    name: 'support tsconfig extends, types and plugins fields',
     module: 'tsconfig',
     options: {},
     expected: {
@@ -247,13 +246,16 @@ export default [
       devDependencies: ['@types/unused'],
       missing: {
         '@types/jest': ['tsconfig.json'],
+        'ts-graphql-plugin': ['tsconfig.json'],
         'tsconfig-base': ['tsconfig.build.json'],
       },
       using: {
         '@mybrand/tsconfig': ['tsconfig.json'],
         '@types/jest': ['tsconfig.json'],
         '@types/node': ['tsconfig.json'],
+        'ts-graphql-plugin': ['tsconfig.json'],
         'tsconfig-base': ['tsconfig.build.json'],
+        'typescript-eslint-language-service': ['tsconfig.json'],
       },
     },
     expectedErrorCode: -1,