Repository: sverweij/dependency-cruiser
Pull request #959 (2024-10-19 11:55:10)

feat(cli): expands the info displayed in --info
## Description

- enriches the --info output with info relevant for troubleshooting environment issues:
 - versions of transpilers _found_
 - current version of dependency-cruiser
 - supported node versions, found node version
 - found os


## Motivation and Context

Makes it easier to debug issues in the setup. E.g. seeing that dependency-cruiser finds typescript@4 is used instead of an expected typescript@5 already gives clues to 


## How Has This Been Tested?

<!--- Please describe in detail how you tested your changes. -->
<!--- Include details of your testing environment, and the tests you ran to -->
<!--- see how your change affects other areas of the code, etc. -->

- [x] green ci

## Screenshots

### After
```
    dependency-cruiser@16.4.2

    node version supported : ^18.17||>=20
    node version found     : v22.8.0
    os version found       : x64 darwin@21.6.0

    If you need a supported, but not enabled transpiler ('x' below), just install
    it in the same folder dependency-cruiser is installed. E.g. 'npm i livescript'
    will enable livescript support if it's installed in your project folder.

    ✔ transpiler             versions supported  version found
    - ---------------------- ------------------- ------------------------
    ✔ javascript             *                   acorn@8.12.1
    ✔ babel                  >=7.0.0 <8.0.0      @babel/core@7.25.2
    ✔ coffee-script          >=1.0.0 <2.0.0      coffeescript@2.7.0
    ✔ coffeescript           >=1.0.0 <3.0.0      coffeescript@2.7.0
    x livescript             >=1.0.0 <2.0.0      -
    ✔ svelte                 >=3.0.0 <5.0.0      svelte/compiler@4.2.19
    ✔ swc                    >=1.0.0 <2.0.0      @swc/core@1.7.26
    ✔ typescript             >=2.0.0 <6.0.0      typescript@5.6.2
    ✔ vue-template-compiler  >=2.0.0 <3.0.0      vue-template-compiler
    ✔ @vue/compiler-sfc      >=3.0.0 <4.0.0      vue-template-compiler

    ✔ extension
    - ---------
    ✔ .js
    ✔ .cjs
    ✔ .mjs
    ✔ .jsx
    ✔ .ts
    ✔ .tsx
    ✔ .d.ts
    ✔ .cts
    ✔ .d.cts
    ✔ .mts
    ✔ .d.mts
    ✔ .vue
    ✔ .svelte
    x .ls
    ✔ .coffee
    ✔ .litcoffee
    ✔ .coffee.md
    ✔ .csx
    ✔ .cjsx
```

### Before

```
 Supported:

    If you need a supported, but not enabled transpiler ('x' below), just install
    it in the same folder dependency-cruiser is installed. E.g. 'npm i livescript'
    will enable livescript support if it's installed in your project folder.

  Transpilers:

    ✔ javascript (>es1)
    ✔ babel (>=7.0.0 <8.0.0)
    ✔ coffee-script (>=1.0.0 <2.0.0)
    ✔ coffeescript (>=1.0.0 <3.0.0)
    x livescript (>=1.0.0 <2.0.0)
    ✔ svelte (>=3.0.0 <5.0.0)
    ✔ swc (>=1.0.0 <2.0.0)
    ✔ typescript (>=2.0.0 <6.0.0)
    ✔ vue-template-compiler (>=2.0.0 <3.0.0)
    ✔ @vue/compiler-sfc (>=3.0.0 <4.0.0)

  Extensions:

    ✔ .js
    ✔ .cjs
    ✔ .mjs
    ✔ .jsx
    ✔ .ts
    ✔ .tsx
    ✔ .d.ts
    ✔ .cts
    ✔ .d.cts
    ✔ .mts
    ✔ .d.mts
    ✔ .vue
    ✔ .svelte
    x .ls
    ✔ .coffee
    ✔ .litcoffee
    ✔ .coffee.md
    ✔ .csx
    ✔ .cjsx
```


## Types of changes

- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] Documentation only change
- [ ] Refactor (non-breaking change which fixes an issue without changing functionality)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to change)

## Checklist

- [x] :book:

  - My change doesn't require a documentation update, or ...
  - it _does_ and I have updated it

- [x] :balance_scale:
  - The contribution will be subject to [The MIT license](https://github.com/sverweij/dependency-cruiser/blob/main/LICENSE), and I'm OK with that.
  - The contribution is my own original work.
  - I am ok with the stuff in [**CONTRIBUTING.md**](https://github.com/sverweij/dependency-cruiser/blob/main/.github/CONTRIBUTING.md).


codeclimate[bot]: Code Climate has analyzed commit c5937fca and detected **0 issues** on this pull request.

View more on [Code Climate](https://codeclimate.com/github/sverweij/dependency-cruiser/pull/959).

codeclimate[bot]: Code Climate has analyzed commit 0106e7f3 and detected **0 issues** on this pull request.

View more on [Code Climate](https://codeclimate.com/github/sverweij/dependency-cruiser/pull/959).

codeclimate[bot]: Code Climate has analyzed commit f92da8ba and detected **0 issues** on this pull request.

View more on [Code Climate](https://codeclimate.com/github/sverweij/dependency-cruiser/pull/959).


Patch:
diff --git a/doc/cli.md b/doc/cli.md
index e8dc4a20..18192c9f 100644
--- a/doc/cli.md
+++ b/doc/cli.md
@@ -22,7 +22,7 @@ available in dependency-cruiser configurations.
 1. [`--init`](#--init)
 1. [`--metrics`: calculate stability metrics](#--metrics)
 1. [`--no-metrics`: do not calculate stability metrics](#--no-metrics)
-1. [`--info`: show what alt-js are supported](#--info-showing-what-alt-js-are-supported)
+1. [`--info`: show what alt-js are supported](#--info-show-what-alt-js-are-supported)
 1. [`--ignore-known`: ignore known violations](#--ignore-known-ignore-known-violations)
 1. [`--no-ignore-known`: don't ignore known violations](#--no-ignore-known)
 1. [`--help`/ no parameters: get help](#--help--no-parameters)
@@ -749,7 +749,7 @@ Do not calculate metrics. You can use this to override an earlier set `--metrics
 command line option or `metrics` option in a .dependency-cruiser.js configuration
 file.
 
-### `--info` showing what alt-js are supported
+### `--info` show what alt-js are supported
 
 Which alt-js languages dependency-cruiser supports depends on the availability
 it has to them. To see how dependency-cruiser perceives its environment use
@@ -759,35 +759,50 @@ it has to them. To see how dependency-cruiser perceives its environment use
 <summary>Typical output</summary>
 
 ```
-Supported:
-
-  If you need a supported, but not enabled transpiler ('✖' below), install
-  it in the same folder dependency-cruiser is installed. E.g. 'npm i livescript'
-  will enable livescript support if it's installed in your project folder.
-
-Transpilers:
-
-  ✔ javascript (>es1)
-  ✔ coffee-script (>=1.0.0 <2.0.0)
-  ✔ coffeescript (>=1.0.0 <3.0.0)
-  ✖ livescript (>=1.0.0 <2.0.0)
-  ✔ typescript (>=2.0.0 <4.0.0)
-
-Extensions:
-
-  ✔ .js
-  ✔ .mjs
-  ✔ .jsx
-  ✔ .vue
-  ✔ .ts
-  ✔ .tsx
-  ✔ .d.ts
-  ✖ .ls
-  ✔ .coffee
-  ✔ .litcoffee
-  ✔ .coffee.md
-  ✔ .csx
-  ✔ .cjsx
+    dependency-cruiser@16.4.2
+
+    node version supported : ^18.17||>=20
+    node version found     : v22.8.0
+    os version found       : x64 darwin@21.6.0
+
+    If you need a supported, but not enabled transpiler ('x' below), just install
+    it in the same folder dependency-cruiser is installed. E.g. 'npm i livescript'
+    will enable livescript support if it's installed in your project folder.
+
+    ✔ transpiler             versions supported  version found
+    - ---------------------- ------------------- ------------------------
+    ✔ javascript             *                   acorn@8.12.1
+    ✔ babel                  >=7.0.0 <8.0.0      @babel/core@7.25.2
+    ✔ coffee-script          >=1.0.0 <2.0.0      coffeescript@2.7.0
+    ✔ coffeescript           >=1.0.0 <3.0.0      coffeescript@2.7.0
+    x livescript             >=1.0.0 <2.0.0      -
+    ✔ svelte                 >=3.0.0 <5.0.0      svelte/compiler@4.2.19
+    ✔ swc                    >=1.0.0 <2.0.0      @swc/core@1.7.26
+    ✔ typescript             >=2.0.0 <6.0.0      typescript@5.6.2
+    ✔ vue-template-compiler  >=2.0.0 <3.0.0      vue-template-compiler
+    ✔ @vue/compiler-sfc      >=3.0.0 <4.0.0      vue-template-compiler
+
+    ✔ extension
+    - ---------
+    ✔ .js
+    ✔ .cjs
+    ✔ .mjs
+    ✔ .jsx
+    ✔ .ts
+    ✔ .tsx
+    ✔ .d.ts
+    ✔ .cts
+    ✔ .d.cts
+    ✔ .mts
+    ✔ .d.mts
+    ✔ .vue
+    ✔ .svelte
+    x .ls
+    ✔ .coffee
+    ✔ .litcoffee
+    ✔ .coffee.md
+    ✔ .csx
+    ✔ .cjsx
 ```
 
 </details>
diff --git a/src/cli/format-meta-info.mjs b/src/cli/format-meta-info.mjs
index dc39e280..c0245c92 100644
--- a/src/cli/format-meta-info.mjs
+++ b/src/cli/format-meta-info.mjs
@@ -1,19 +1,29 @@
+import { release, platform, arch } from "node:os";
 import pc from "picocolors";
 
 import { getAvailableTranspilers, allExtensions } from "#main/index.mjs";
+import meta from "#meta.cjs";
 
 function bool2Symbol(pBool) {
   return pBool ? pc.green("✔") : pc.red("x");
 }
 
+const MAX_VERSION_RANGE_STRING_LENGTH = 19;
+const MAX_TRANSPILER_NAME_LENGTH = 22;
+const MAX_VERSION_STRING_LENGTH = 24;
+
 function formatTranspilers() {
-  return getAvailableTranspilers().reduce(
-    (pAll, pThis) =>
-      `${pAll}    ${bool2Symbol(pThis.available)} ${pThis.name} (${
-        pThis.version
-      })\n`,
-    `    ${bool2Symbol(true)} javascript (>es1)\n`,
+  let lTranspilerTableHeader = pc.bold(
+    `    ✔ ${"transpiler".padEnd(MAX_TRANSPILER_NAME_LENGTH)} ${"versions supported".padEnd(MAX_VERSION_RANGE_STRING_LENGTH)} version found`,
   );
+  let lTranspilerTableDivider = `    - ${"-".repeat(MAX_TRANSPILER_NAME_LENGTH)} ${"-".repeat(MAX_VERSION_RANGE_STRING_LENGTH)} ${"-".repeat(MAX_VERSION_STRING_LENGTH)}`;
+  let lTranspilerTable = getAvailableTranspilers()
+    .map(
+      (pTranspiler) =>
+        `    ${bool2Symbol(pTranspiler.available)} ${pTranspiler.name.padEnd(MAX_TRANSPILER_NAME_LENGTH)} ${pTranspiler.version.padEnd(MAX_VERSION_RANGE_STRING_LENGTH)} ${pTranspiler.currentVersion}`,
+    )
+    .join("\n");
+  return `${lTranspilerTableHeader}\n${lTranspilerTableDivider}\n${lTranspilerTable}\n`;
 }
 
 function formatExtensions(pExtensions) {
@@ -26,7 +36,11 @@ function formatExtensions(pExtensions) {
 
 export default function formatMetaInfo() {
   return `
-  Supported:
+    ${pc.bold("dependency-cruiser")}@${meta.version}
+
+    node version supported : ${meta.engines.node}
+    node version found     : ${process.version}
+    os version found       : ${arch()} ${platform()}@${release()}
 
     If you need a supported, but not enabled transpiler ('${pc.red(
       "x",
@@ -34,11 +48,9 @@ export default function formatMetaInfo() {
     it in the same folder dependency-cruiser is installed. E.g. 'npm i livescript'
     will enable livescript support if it's installed in your project folder.
 
-  Transpilers:
-
 ${formatTranspilers()}
-  Extensions:
-
+    ${pc.bold("✔ extension")}
+    - ---------
 ${formatExtensions(allExtensions)}
 `;
 }
diff --git a/src/extract/swc/parse.mjs b/src/extract/swc/parse.mjs
index 51cfae69..1d85c023 100644
--- a/src/extract/swc/parse.mjs
+++ b/src/extract/swc/parse.mjs
@@ -46,3 +46,5 @@ export function clearCache() {
  */
 // @ts-expect-error dfdfd
 export const isAvailable = () => swc !== false;
+
+export const version = () => `@swc/core@${swc.version}`;
diff --git a/src/extract/transpile/babel-wrap.mjs b/src/extract/transpile/babel-wrap.mjs
index 207568ee..cace0227 100644
--- a/src/extract/transpile/babel-wrap.mjs
+++ b/src/extract/transpile/babel-wrap.mjs
@@ -5,6 +5,9 @@ const babel = await tryImport("@babel/core", meta.supportedTranspilers.babel);
 
 export default {
   isAvailable: () => babel !== false,
+
+  version: () => `@babel/core@${babel.version}`,
+
   transpile: (pSource, pFileName, pTranspileOptions = {}) =>
     babel.transformSync(pSource, {
       ...(pTranspileOptions.babelConfig || {}),
diff --git a/src/extract/transpile/coffeescript-wrap.mjs b/src/extract/transpile/coffeescript-wrap.mjs
index 5890674e..43a5cf1c 100644
--- a/src/extract/transpile/coffeescript-wrap.mjs
+++ b/src/extract/transpile/coffeescript-wrap.mjs
@@ -28,6 +28,9 @@ const coffeeScript = await getCoffeeScriptModule();
 export default function coffeeScriptWrap(pLiterate) {
   return {
     isAvailable: () => coffeeScript !== false,
+
+    version: () => `coffeescript@${coffeeScript.VERSION}`,
+
     transpile: (pSource) => {
       const lOptions = pLiterate ? { literate: true } : {};
 
diff --git a/src/extract/transpile/javascript-wrap.mjs b/src/extract/transpile/javascript-wrap.mjs
index 2aa860c2..33244b6a 100644
--- a/src/extract/transpile/javascript-wrap.mjs
+++ b/src/extract/transpile/javascript-wrap.mjs
@@ -1,4 +1,12 @@
+import { createRequire } from "node:module";
+
 export default {
   isAvailable: () => true,
+
+  version: () => {
+    const require = createRequire(import.meta.url);
+    return `acorn@${require("acorn").version}`;
+  },
+
   transpile: (pSource) => pSource,
 };
diff --git a/src/extract/transpile/livescript-wrap.mjs b/src/extract/transpile/livescript-wrap.mjs
index af5e74ad..c2cc857f 100644
--- a/src/extract/transpile/livescript-wrap.mjs
+++ b/src/extract/transpile/livescript-wrap.mjs
@@ -10,6 +10,8 @@ const livescript = await tryImport(
 export default {
   isAvailable: () => livescript !== false,
 
+  version: () => `livescript@${livescript.VERSION}`,
+
   transpile: (pSource) => livescript.compile(pSource),
 };
 /* c8 ignore stop */
diff --git a/src/extract/transpile/meta.mjs b/src/extract/transpile/meta.mjs
index ae5cd9af..48278628 100644
--- a/src/extract/transpile/meta.mjs
+++ b/src/extract/transpile/meta.mjs
@@ -1,7 +1,26 @@
 /* eslint security/detect-object-injection : 0*/
+import {
+  isAvailable as swcIsAvailable,
+  version as swcVersion,
+} from "../swc/parse.mjs";
 import tryAvailable from "./try-import-available.mjs";
+import javascriptWrap from "./javascript-wrap.mjs";
+import babelWrap from "./babel-wrap.mjs";
+import coffeeScriptWrapFunction from "./coffeescript-wrap.mjs";
+import livescriptWrap from "./livescript-wrap.mjs";
+import svelteWrapFunction from "./svelte-wrap.mjs";
+import typescriptWrapFunction from "./typescript-wrap.mjs";
+import vueTemplateWrap from "./vue-template-wrap.cjs";
 import meta from "#meta.cjs";
 
+const swcWrap = {
+  isAvailable: () => swcIsAvailable(),
+  version: () => swcVersion(),
+};
+const coffeeScriptWrap = coffeeScriptWrapFunction(false);
+const svelteWrap = svelteWrapFunction(javascriptWrap);
+const typescriptWrap = typescriptWrapFunction("esm");
+
 function gotCoffee() {
   return (
     tryAvailable("coffeescript", meta.supportedTranspilers.coffeescript) ||
@@ -28,6 +47,19 @@ const TRANSPILER2AVAILABLE = {
   ),
 };
 
+const TRANSPILER2WRAPPER = {
+  babel: babelWrap,
+  javascript: javascriptWrap,
+  "coffee-script": coffeeScriptWrap,
+  coffeescript: coffeeScriptWrap,
+  livescript: livescriptWrap,
+  svelte: svelteWrap,
+  swc: swcWrap,
+  typescript: typescriptWrap,
+  "vue-template-compiler": vueTemplateWrap,
+  "@vue/compiler-sfc": vueTemplateWrap,
+};
+
 export const EXTENSION2AVAILABLE = new Map([
   [".js", TRANSPILER2AVAILABLE.javascript],
   [".cjs", TRANSPILER2AVAILABLE.javascript],
@@ -81,6 +113,11 @@ export const allExtensions = Array.from(EXTENSION2AVAILABLE.keys()).map(
   }),
 );
 
+function getCurrentVersion(pTranspiler) {
+  const lTranspiler = TRANSPILER2WRAPPER[pTranspiler];
+  return lTranspiler.isAvailable() ? lTranspiler.version() : "-";
+}
+
 /**
  * an array of extensions that are 'scannable' (have a valid transpiler
  * available for) in the current environment.
@@ -99,9 +136,12 @@ export const scannableExtensions = Array.from(
  * @return {IAvailableTranspiler[]} an array of supported transpilers
  */
 export function getAvailableTranspilers() {
-  return Object.keys(meta.supportedTranspilers).map((pTranspiler) => ({
-    name: pTranspiler,
-    version: meta.supportedTranspilers[pTranspiler],
-    available: TRANSPILER2AVAILABLE[pTranspiler],
-  }));
+  return ["javascript"]
+    .concat(Object.keys(meta.supportedTranspilers))
+    .map((pTranspiler) => ({
+      name: pTranspiler,
+      version: meta.supportedTranspilers[pTranspiler] || "*",
+      available: TRANSPILER2AVAILABLE[pTranspiler],
+      currentVersion: getCurrentVersion(pTranspiler),
+    }));
 }
diff --git a/src/extract/transpile/svelte-wrap.mjs b/src/extract/transpile/svelte-wrap.mjs
index fb48b130..022046e3 100644
--- a/src/extract/transpile/svelte-wrap.mjs
+++ b/src/extract/transpile/svelte-wrap.mjs
@@ -2,7 +2,7 @@ import preProcess from "./svelte-preprocess.mjs";
 import tryImport from "#utl/try-import.mjs";
 import meta from "#meta.cjs";
 
-const { compile } = await tryImport(
+const { compile, VERSION } = await tryImport(
   "svelte/compiler",
   meta.supportedTranspilers.svelte,
 );
@@ -21,6 +21,7 @@ function getTranspiler(pTranspilerWrapper) {
 export default function svelteWrap(pTranspilerWrapper) {
   return {
     isAvailable: () => Boolean(compile),
+    version: () => `svelte/compiler@${VERSION}`,
     transpile: getTranspiler(pTranspilerWrapper),
   };
 }
diff --git a/src/extract/transpile/typescript-wrap.mjs b/src/extract/transpile/typescript-wrap.mjs
index 016b03b0..b59ddbea 100644
--- a/src/extract/transpile/typescript-wrap.mjs
+++ b/src/extract/transpile/typescript-wrap.mjs
@@ -36,6 +36,8 @@ export default function typescriptWrap(pFlavor) {
   return {
     isAvailable: () => typescript !== false,
 
+    version: () => `typescript@${typescript.version}`,
+
     transpile: (pSource, _pFileName, pTranspileOptions = {}) =>
       typescript.transpileModule(pSource, {
         ...(pTranspileOptions.tsConfig || {}),
diff --git a/src/extract/transpile/vue-template-wrap.cjs b/src/extract/transpile/vue-template-wrap.cjs
index 1d5f2df7..e43283a5 100644
--- a/src/extract/transpile/vue-template-wrap.cjs
+++ b/src/extract/transpile/vue-template-wrap.cjs
@@ -71,6 +71,10 @@ function vue2Transpile(pSource) {
 
 module.exports = {
   isAvailable: () => vueTemplateCompiler !== false,
+  version: () =>
+    vueTemplateCompiler.version
+      ? `@vue/compiler-sfc@${vueTemplateCompiler.version}`
+      : "vue-template-compiler",
   transpile: (pSource) =>
     isVue3 ? vue3Transpile(pSource) : vue2Transpile(pSource),
 };


Test patch:
diff --git a/test/extract/transpile/meta.spec.mjs b/test/extract/transpile/meta.spec.mjs
index 3fc56b3b..ba5d9073 100644
--- a/test/extract/transpile/meta.spec.mjs
+++ b/test/extract/transpile/meta.spec.mjs
@@ -1,4 +1,5 @@
 import { deepEqual } from "node:assert/strict";
+import { validRange } from "semver";
 import {
   getAvailableTranspilers,
   scannableExtensions,
@@ -29,52 +30,38 @@ describe("[U] extract/transpile/meta", () => {
   });
 
   it("returns the available transpilers", () => {
-    deepEqual(getAvailableTranspilers(), [
-      {
-        name: "babel",
-        version: ">=7.0.0 <8.0.0",
-        available: true,
-      },
-      {
-        name: "coffee-script",
-        version: ">=1.0.0 <2.0.0",
-        available: true,
-      },
-      {
-        name: "coffeescript",
-        version: ">=1.0.0 <3.0.0",
-        available: true,
-      },
-      {
-        name: "livescript",
-        version: ">=1.0.0 <2.0.0",
-        available: false,
-      },
-      {
-        name: "svelte",
-        version: ">=3.0.0 <5.0.0",
-        available: true,
-      },
-      {
-        name: "swc",
-        version: ">=1.0.0 <2.0.0",
-        available: true,
-      },
-      {
-        name: "typescript",
-        version: ">=2.0.0 <6.0.0",
-        available: true,
-      },
-      {
-        name: "vue-template-compiler",
-        version: ">=2.0.0 <3.0.0",
-        available: true,
-      },
-      {
-        name: "@vue/compiler-sfc",
-        version: ">=3.0.0 <4.0.0",
-        available: true,
-      },
-    ]);
+    const lTranspilers = getAvailableTranspilers();
+    deepEqual(Array.isArray(lTranspilers), true);
+    deepEqual(lTranspilers.length > 0, true);
+
+    lTranspilers.forEach((pTranspiler) => {
+      deepEqual(
+        typeof pTranspiler.name,
+        "string",
+        `name is not a string: ${pTranspiler.name}`,
+      );
+      deepEqual(
+        typeof pTranspiler.version,
+        "string",
+        `version is not a string: ${pTranspiler.version}`,
+      );
+      deepEqual(
+        validRange(pTranspiler.version),
+        pTranspiler.version,
+        `version is not a valid semver range: ${pTranspiler.version}`,
+      );
+      deepEqual(
+        typeof pTranspiler.currentVersion,
+        "string",
+        `version is not a string: ${pTranspiler.version}`,
+      );
+      deepEqual(
+        typeof pTranspiler.available,
+        "boolean",
+        `available is not a boolean: ${pTranspiler.available}`,
+      );
+      // eslint-disable-next-line no-magic-numbers
+      deepEqual(Object.keys(pTranspiler).length, 4);
+    });
   });
 });