Repository: depcheck/depcheck (js)

Issue:
Support babel preset scope shorthand
### Feature request description

In babel config, you can specify a preset as `@foo` and babel will search for a package named `@foo/babel-preset` instead.


Pull request description:
Support Babel presets/plugins from scoped packages
Babel name normalization docs: https://babeljs.io/docs/options#name-normalization

Fixes #719 

Patch:
diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml
index f2e4f84..0cf998b 100644
--- a/.github/workflows/nodejs.yml
+++ b/.github/workflows/nodejs.yml
@@ -7,7 +7,7 @@ jobs:
     strategy:
       matrix:
         os: [ubuntu-latest]
-        node-version: [14.x, 16.x, 18.x, 20.x]
+        node-version: [16.x, 18.x, 20.x]
 
     runs-on: ${{ matrix.os }}
 
diff --git a/src/special/babel.js b/src/special/babel.js
index 0e067f2..05d01a6 100755
--- a/src/special/babel.js
+++ b/src/special/babel.js
@@ -17,19 +17,32 @@ function isPlugin(target, plugin) {
     : target[0] === plugin || target[0] === `babel-plugin-${plugin}`;
 }
 
-function contain(array, dep, prefix, scope) {
+function contain(array, dep, prefix, babelScope) {
   if (!array) {
     return false;
   }
 
   // extract name if wrapping with options
   const names = array.map((item) => (lodash.isString(item) ? item : item[0]));
-  if (names.indexOf(dep) !== -1) {
+  if (names.some((name) => name.startsWith(dep))) {
     return true;
   }
 
+  // Parse a valid scope from dep if babelScope not defined
+  const scopeMatch = dep.match(/^@[\w-]+/);
+  const scope = babelScope ?? (scopeMatch && scopeMatch.at(0));
+
   const fullPrefix = scope ? `${scope}/${prefix}` : prefix;
 
+  // Support scoped plugin/preset with default name, e.g. preset with name '@scope' uses '@scope/babel-preset' dep
+  if (
+    fullPrefix &&
+    fullPrefix.endsWith('-') &&
+    dep === fullPrefix.slice(0, -1)
+  ) {
+    return true;
+  }
+
   if (prefix && dep.indexOf(fullPrefix) === 0) {
     const identifier = dep.substring(fullPrefix.length);
     return contain(array, scope ? `${scope}/${identifier}` : identifier, false);


Test patch:
diff --git a/test/special/babel.js b/test/special/babel.js
index c092ed0..a5abd80 100755
--- a/test/special/babel.js
+++ b/test/special/babel.js
@@ -39,33 +39,96 @@ const testCases = [
     },
   },
   {
-    name: 'recognize the scoped short-name plugin',
+    name: 'recognize the @babel-scoped short-name plugin',
     deps: ['@babel/plugin-syntax-jsx'],
     options: {
       plugins: ['@babel/syntax-jsx'],
     },
   },
   {
-    name: 'recognize the scoped long-name plugin',
+    name: 'recognize the @babel-scoped long-name plugin',
     deps: ['@babel/plugin-syntax-jsx'],
     options: {
       plugins: ['@babel/plugin-syntax-jsx'],
     },
   },
   {
-    name: 'recognize the scoped short-name preset',
+    name: 'recognize the scoped short-name plugin',
+    deps: ['@scope/babel-plugin'],
+    options: {
+      plugins: ['@scope'],
+    },
+  },
+  {
+    name: 'recognize the scoped short-name plugin with custom suffix',
+    deps: ['@scope/babel-plugin-syntax-jsx'],
+    options: {
+      plugins: ['@scope/syntax-jsx'],
+    },
+  },
+  {
+    name: 'recognize the @babel-scoped short-name preset',
     deps: ['@babel/preset-es2015'],
     options: {
       presets: ['@babel/es2015'],
     },
   },
   {
-    name: 'recognize the scoped long-name preset',
+    name: 'recognize the @babel-scoped long-name preset',
     deps: ['@babel/preset-es2015'],
     options: {
       presets: ['@babel/preset-es2015'],
     },
   },
+  {
+    name: 'recognize the scoped short-name preset',
+    deps: ['@scope/babel-preset'],
+    options: {
+      presets: ['@scope'],
+    },
+  },
+  {
+    name: 'recognize the scoped short-name preset with custom suffix',
+    deps: ['@scope/babel-preset-es2015'],
+    options: {
+      presets: ['@scope/es2015'],
+    },
+  },
+  {
+    name: 'recognize the scoped long-name preset',
+    deps: ['@scope/babel-preset-es2015'],
+    options: {
+      presets: ['@scope/babel-preset-es2015'],
+    },
+  },
+  {
+    name: 'recognize the scoped preset with file path reference',
+    deps: ['@scope/mod'],
+    options: {
+      presets: ['@scope/mod/preset'],
+    },
+  },
+  {
+    name: 'recognize preset with file path reference',
+    deps: ['mod'],
+    options: {
+      presets: ['mod/preset'],
+    },
+  },
+  {
+    name: 'recognize the scoped plugin with file path reference',
+    deps: ['@scope/mod'],
+    options: {
+      plugins: ['@scope/mod/plugin'],
+    },
+  },
+  {
+    name: 'recognize plugin with file path reference',
+    deps: ['mod'],
+    options: {
+      plugins: ['mod/plugin'],
+    },
+  },
   {
     name: 'recognize plugin specified with options',
     deps: ['babel-plugin-transform-async-to-module-method'],