Repository: sverweij/dependency-cruiser
Pull request #846 (2023-09-29 13:10:10)

feat(init): adds 'module' field to enhancedResolve mainFields when current module is type:module
## Description

- If the current package.json contains a `type` attribute with the value `module` add `module` to the list of mainFields enhanced resolve should use in its (external) module resolution.
- Makes the default mainFields `['main', 'types', 'typings']` over just `['main']` (or a comment) as it's _always_ useful in TypeScript project (which is likely in the majority of all JavaScript projects out there) and doesn't hurt in other contexts.


Additionally, a separate commit, outside this PR, documents the working of the mainFields attribute for ESM contexts (b92b79bd)


## Motivation and Context

#843 

## How Has This Been Tested?

- [x] green ci
- [x] additional automated tests

## 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 757fca44 and detected **0 issues** on this pull request.

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


Patch:
diff --git a/src/cli/init-config/build-config.mjs b/src/cli/init-config/build-config.mjs
index 788cfd98..0bbf84ad 100644
--- a/src/cli/init-config/build-config.mjs
+++ b/src/cli/init-config/build-config.mjs
@@ -135,9 +135,10 @@ function buildExtensionsAttribute(pInitOptions) {
  * @returns {string}
  */
 function buildMainFieldsAttribute(pInitOptions) {
-  return pInitOptions.usesTypeScript
-    ? `mainFields: ["main", "types", "typings"],`
-    : `// mainFields: ["main", "types", "typings"],`;
+  if (pInitOptions.isTypeModule) {
+    return `mainFields: ["module", "main", "types", "typings"],`;
+  }
+  return `mainFields: ["main", "types", "typings"],`;
 }
 
 /**
diff --git a/src/cli/init-config/get-user-input.mjs b/src/cli/init-config/get-user-input.mjs
index 0f92f0fb..f13f9c64 100644
--- a/src/cli/init-config/get-user-input.mjs
+++ b/src/cli/init-config/get-user-input.mjs
@@ -2,6 +2,7 @@
 import prompts from "prompts";
 import {
   isLikelyMonoRepo,
+  isTypeModule,
   getMonoRepoPackagesCandidates,
   getSourceFolderCandidates,
   getTestFolderCandidates,
@@ -33,6 +34,12 @@ const QUESTIONS = [
     message: "This looks like mono repo. Is that correct?",
     initial: isLikelyMonoRepo(),
   },
+  {
+    name: "isTypeModule",
+    type: () => (isTypeModule() ? "confirm" : false),
+    message: "It looks like this is an ESM package. Is that correct?",
+    initial: isTypeModule(),
+  },
   {
     name: "sourceLocation",
     type: (_, pAnswers) => (pAnswers.isMonoRepo ? "list" : false),
@@ -61,7 +68,7 @@ const QUESTIONS = [
     initial: (_, pAnswers) => {
       return !hasTestsWithinSource(
         getTestFolderCandidates(),
-        toSourceLocationArray(pAnswers.sourceLocation)
+        toSourceLocationArray(pAnswers.sourceLocation),
       );
     },
   },
diff --git a/src/cli/init-config/index.mjs b/src/cli/init-config/index.mjs
index f705f52d..12d8d91a 100644
--- a/src/cli/init-config/index.mjs
+++ b/src/cli/init-config/index.mjs
@@ -16,6 +16,7 @@ import {
   getDefaultConfigFileName,
   hasJSConfigCandidates,
   getJSConfigCandidates,
+  isTypeModule,
 } from "./environment-helpers.mjs";
 import { writeRunScriptsToManifest } from "./write-run-scripts-to-manifest.mjs";
 
@@ -32,6 +33,7 @@ function getOneShotConfig(pOneShotConfigId) {
   /** @type {import("./types").IPartialInitConfig} */
   const lBaseConfig = {
     isMonoRepo: isLikelyMonoRepo(),
+    isTypeModule: isTypeModule(),
     combinedDependencies: false,
     useJsConfig: hasJSConfigCandidates() && !hasTSConfigCandidates(),
     jsConfig: getJSConfigCandidates().shift(),
diff --git a/src/cli/init-config/types.d.ts b/src/cli/init-config/types.d.ts
index bcbfb7ea..f3fa78e5 100644
--- a/src/cli/init-config/types.d.ts
+++ b/src/cli/init-config/types.d.ts
@@ -5,6 +5,12 @@ export interface IInitConfig {
    * Whether or not the current folder houses a mono repo
    */
   isMonoRepo: boolean;
+  /**
+   * Whether or not the current folder is a package is an ESM package
+   * by default (and resolutions of external dependencies should be
+   * treated as such)
+   */
+  isTypeModule: boolean;
   /**
    * Whether or not you allow usage of external dependencies declared in
    * package.jsons of parent folders


Test patch:
diff --git a/test/cli/init-config/build-config.spec.mjs b/test/cli/init-config/build-config.spec.mjs
index 1b814adb..3ad2ee94 100644
--- a/test/cli/init-config/build-config.spec.mjs
+++ b/test/cli/init-config/build-config.spec.mjs
@@ -101,4 +101,29 @@ describe("[I] cli/init-config/build-config", () => {
       fileName: "./tsconfig.json",
     });
   });
+
+  it("generates mainFields including 'module' for package.json with a type: module field", async () => {
+    const lResult = await createConfigNormalized({ isTypeModule: true });
+
+    ajv.validate(configurationSchema, lResult);
+    ok(lResult.hasOwnProperty("options"));
+    deepEqual(lResult.options.enhancedResolveOptions.mainFields, [
+      "module",
+      "main",
+      "types",
+      "typings",
+    ]);
+  });
+
+  it("generates mainFields including 'module' for package.json withOUT a type: module field", async () => {
+    const lResult = await createConfigNormalized({});
+
+    ajv.validate(configurationSchema, lResult);
+    ok(lResult.hasOwnProperty("options"));
+    deepEqual(lResult.options.enhancedResolveOptions.mainFields, [
+      "main",
+      "types",
+      "typings",
+    ]);
+  });
 });