Repository: jhipster/jhipster-lite
Pull request #10467 (2024-07-31 13:34:42)

feat(packagejson): Use a proper typed enum for the package type
aka the node module format


Patch:
diff --git a/src/main/java/tech/jhipster/lite/generator/client/react/security/jwt/domain/ReactJwtModuleFactory.java b/src/main/java/tech/jhipster/lite/generator/client/react/security/jwt/domain/ReactJwtModuleFactory.java
index e89639555..62cf2bfcf 100644
--- a/src/main/java/tech/jhipster/lite/generator/client/react/security/jwt/domain/ReactJwtModuleFactory.java
+++ b/src/main/java/tech/jhipster/lite/generator/client/react/security/jwt/domain/ReactJwtModuleFactory.java
@@ -1,6 +1,7 @@
 package tech.jhipster.lite.generator.client.react.security.jwt.domain;
 
 import static tech.jhipster.lite.module.domain.JHipsterModule.*;
+import static tech.jhipster.lite.module.domain.packagejson.NodeModuleFormat.MODULE;
 import static tech.jhipster.lite.module.domain.packagejson.VersionSource.COMMON;
 import static tech.jhipster.lite.module.domain.packagejson.VersionSource.REACT;
 
@@ -45,7 +46,7 @@ public class ReactJwtModuleFactory {
     //@formatter:off
     return moduleBuilder(properties)
       .packageJson()
-        .addType("module")
+        .type(MODULE)
         .addDependency(packageName("react-hook-form"), REACT)
         .addDependency(packageName("axios"), REACT)
         .addDependency(packageName("@nextui-org/react"), REACT)
diff --git a/src/main/java/tech/jhipster/lite/generator/client/vue/core/domain/VueModulesFactory.java b/src/main/java/tech/jhipster/lite/generator/client/vue/core/domain/VueModulesFactory.java
index 8f419488d..386acf200 100644
--- a/src/main/java/tech/jhipster/lite/generator/client/vue/core/domain/VueModulesFactory.java
+++ b/src/main/java/tech/jhipster/lite/generator/client/vue/core/domain/VueModulesFactory.java
@@ -1,6 +1,7 @@
 package tech.jhipster.lite.generator.client.vue.core.domain;
 
 import static tech.jhipster.lite.module.domain.JHipsterModule.*;
+import static tech.jhipster.lite.module.domain.packagejson.NodeModuleFormat.MODULE;
 import static tech.jhipster.lite.module.domain.packagejson.VersionSource.COMMON;
 import static tech.jhipster.lite.module.domain.packagejson.VersionSource.VUE;
 
@@ -49,7 +50,7 @@ public class VueModulesFactory {
         .and()
       .documentation(documentationTitle("Vue"), DOCUMENTATION_SOURCE.file("vue.md"))
       .packageJson()
-        .addType("module")
+        .type(MODULE)
         .addDependency(packageName("vue"), VUE)
         .addDependency(packageName("axios"), VUE)
         .addDependency(packageName("vue-router"), VUE)
diff --git a/src/main/java/tech/jhipster/lite/generator/typescript/core/domain/TypescriptModuleFactory.java b/src/main/java/tech/jhipster/lite/generator/typescript/core/domain/TypescriptModuleFactory.java
index 8b7e41102..0ea4f9b09 100644
--- a/src/main/java/tech/jhipster/lite/generator/typescript/core/domain/TypescriptModuleFactory.java
+++ b/src/main/java/tech/jhipster/lite/generator/typescript/core/domain/TypescriptModuleFactory.java
@@ -1,6 +1,7 @@
 package tech.jhipster.lite.generator.typescript.core.domain;
 
 import static tech.jhipster.lite.module.domain.JHipsterModule.*;
+import static tech.jhipster.lite.module.domain.packagejson.NodeModuleFormat.MODULE;
 import static tech.jhipster.lite.module.domain.packagejson.VersionSource.COMMON;
 
 import tech.jhipster.lite.module.domain.JHipsterModule;
@@ -18,7 +19,7 @@ public class TypescriptModuleFactory {
     //@formatter:off
     return moduleBuilder(properties)
       .packageJson()
-        .addType("module")
+        .type(MODULE)
         .addDevDependency(packageName("typescript"), COMMON)
         .addDevDependency(packageName("@tsconfig/recommended"), COMMON)
         .addDevDependency(packageName("@typescript-eslint/eslint-plugin"), COMMON)
diff --git a/src/main/java/tech/jhipster/lite/module/domain/packagejson/JHipsterModulePackageJson.java b/src/main/java/tech/jhipster/lite/module/domain/packagejson/JHipsterModulePackageJson.java
index 748960318..eaf537b8a 100644
--- a/src/main/java/tech/jhipster/lite/module/domain/packagejson/JHipsterModulePackageJson.java
+++ b/src/main/java/tech/jhipster/lite/module/domain/packagejson/JHipsterModulePackageJson.java
@@ -1,7 +1,7 @@
 package tech.jhipster.lite.module.domain.packagejson;
 
-import java.util.ArrayList;
-import java.util.Collection;
+import java.util.*;
+import java.util.stream.Stream;
 import tech.jhipster.lite.module.domain.JHipsterModule.JHipsterModuleBuilder;
 import tech.jhipster.lite.shared.error.domain.Assert;
 import tech.jhipster.lite.shared.generation.domain.ExcludeFromGeneratedCodeCoverage;
@@ -18,7 +18,7 @@ public final class JHipsterModulePackageJson {
   private final PackageNames dependenciesToRemove;
   private final PackageJsonDependencies devDependencies;
   private final PackageNames devDependenciesToRemove;
-  private final PackageJsonType type;
+  private final Optional<NodeModuleFormat> nodeModuleFormat;
 
   private JHipsterModulePackageJson(JHipsterModulePackageJsonBuilder builder) {
     scripts = new Scripts(builder.scripts);
@@ -26,7 +26,7 @@ public final class JHipsterModulePackageJson {
     dependenciesToRemove = new PackageNames(builder.dependenciesToRemove);
     devDependencies = new PackageJsonDependencies(builder.devDependencies);
     devDependenciesToRemove = new PackageNames(builder.devDependenciesToRemove);
-    type = new PackageJsonType(builder.type);
+    nodeModuleFormat = Optional.ofNullable(builder.nodeModuleFormat);
   }
 
   public static JHipsterModulePackageJsonBuilder builder(JHipsterModuleBuilder module) {
@@ -35,6 +35,7 @@ public final class JHipsterModulePackageJson {
 
   public boolean isEmpty() {
     return (
+      nodeModuleFormat.isEmpty() &&
       scripts.isEmpty() &&
       dependencies.isEmpty() &&
       devDependencies.isEmpty() &&
@@ -63,8 +64,8 @@ public final class JHipsterModulePackageJson {
     return dependenciesToRemove;
   }
 
-  public PackageJsonType type() {
-    return type;
+  public Optional<NodeModuleFormat> nodeModuleFormat() {
+    return nodeModuleFormat;
   }
 
   public static final class JHipsterModulePackageJsonBuilder {
@@ -75,7 +76,7 @@ public final class JHipsterModulePackageJson {
     private final Collection<PackageJsonDependency> devDependencies = new ArrayList<>();
     private final Collection<PackageName> dependenciesToRemove = new ArrayList<>();
     private final Collection<PackageName> devDependenciesToRemove = new ArrayList<>();
-    private String type;
+    private NodeModuleFormat nodeModuleFormat;
 
     private JHipsterModulePackageJsonBuilder(JHipsterModuleBuilder module) {
       Assert.notNull("module", module);
@@ -217,11 +218,28 @@ public final class JHipsterModulePackageJson {
     /**
      * Add a type to the {@code package.json}.
      *
-     * @param t the type
+     * @param moduleFormat the moduleFormat
      * @return the builder itself
+     * @deprecated Use {@link #type(NodeModuleFormat)} instead
      */
-    public JHipsterModulePackageJsonBuilder addType(String t) {
-      type = t;
+    @Deprecated(forRemoval = true, since = "1.17.0")
+    public JHipsterModulePackageJsonBuilder addType(String moduleFormat) {
+      nodeModuleFormat = Stream.of(NodeModuleFormat.values())
+        .filter(enumValue -> enumValue.name().equalsIgnoreCase(moduleFormat))
+        .findFirst()
+        .orElseThrow();
+
+      return this;
+    }
+
+    /**
+     * Defines the module format ("type") in the {@code package.json}.
+     *
+     * @param moduleFormat the module format
+     * @return the builder itself
+     */
+    public JHipsterModulePackageJsonBuilder type(NodeModuleFormat moduleFormat) {
+      nodeModuleFormat = moduleFormat;
 
       return this;
     }
diff --git a/src/main/java/tech/jhipster/lite/module/domain/packagejson/NodeModuleFormat.java b/src/main/java/tech/jhipster/lite/module/domain/packagejson/NodeModuleFormat.java
new file mode 100644
index 000000000..4ded68779
--- /dev/null
+++ b/src/main/java/tech/jhipster/lite/module/domain/packagejson/NodeModuleFormat.java
@@ -0,0 +1,9 @@
+package tech.jhipster.lite.module.domain.packagejson;
+
+/**
+ * See <a href="https://nodejs.org/api/packages.html#type">NodeJS documentation</a>
+ */
+public enum NodeModuleFormat {
+  COMMONJS,
+  MODULE,
+}
diff --git a/src/main/java/tech/jhipster/lite/module/domain/packagejson/PackageJsonType.java b/src/main/java/tech/jhipster/lite/module/domain/packagejson/PackageJsonType.java
deleted file mode 100644
index 0f5c29525..000000000
--- a/src/main/java/tech/jhipster/lite/module/domain/packagejson/PackageJsonType.java
+++ /dev/null
@@ -1,3 +0,0 @@
-package tech.jhipster.lite.module.domain.packagejson;
-
-public record PackageJsonType(String type) {}
diff --git a/src/main/java/tech/jhipster/lite/module/infrastructure/secondary/FileSystemPackageJsonHandler.java b/src/main/java/tech/jhipster/lite/module/infrastructure/secondary/FileSystemPackageJsonHandler.java
index 331a0b79e..45aacbe66 100644
--- a/src/main/java/tech/jhipster/lite/module/infrastructure/secondary/FileSystemPackageJsonHandler.java
+++ b/src/main/java/tech/jhipster/lite/module/infrastructure/secondary/FileSystemPackageJsonHandler.java
@@ -4,8 +4,7 @@ import static tech.jhipster.lite.module.domain.JHipsterModule.LINE_BREAK;
 
 import java.io.IOException;
 import java.nio.file.*;
-import java.util.Collection;
-import java.util.List;
+import java.util.*;
 import java.util.function.Function;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
@@ -18,6 +17,7 @@ import tech.jhipster.lite.module.domain.npm.NpmVersionSource;
 import tech.jhipster.lite.module.domain.npm.NpmVersions;
 import tech.jhipster.lite.module.domain.packagejson.*;
 import tech.jhipster.lite.module.domain.properties.JHipsterProjectFolder;
+import tech.jhipster.lite.shared.collection.domain.JHipsterCollections;
 import tech.jhipster.lite.shared.enumeration.domain.Enums;
 import tech.jhipster.lite.shared.error.domain.Assert;
 import tech.jhipster.lite.shared.error.domain.GeneratorException;
@@ -59,7 +59,7 @@ class FileSystemPackageJsonHandler {
     Path file = getPackageJsonFile(projectFolder);
 
     String content = readContent(file);
-    content = replaceType(indentation, packageJson.type(), content);
+    content = replaceType(indentation, packageJson.nodeModuleFormat(), content);
     content = replaceScripts(indentation, packageJson.scripts(), content);
     content = replaceDevDependencies(indentation, packageJson.devDependencies(), content);
     content = replaceDependencies(indentation, packageJson.dependencies(), content);
@@ -134,8 +134,16 @@ class FileSystemPackageJsonHandler {
       .apply();
   }
 
-  private String replaceType(Indentation indentation, PackageJsonType packageJsonType, String content) {
-    return JsonAction.replace().blockName("type").jsonContent(content).indentation(indentation).blockValue(packageJsonType.type()).apply();
+  private String replaceType(Indentation indentation, Optional<NodeModuleFormat> nodeModuleFormat, String content) {
+    if (nodeModuleFormat.isEmpty()) {
+      return content;
+    }
+    return JsonAction.replace()
+      .blockName("type")
+      .jsonContent(content)
+      .indentation(indentation)
+      .blockValue(nodeModuleFormat.orElseThrow().name().toLowerCase())
+      .apply();
   }
 
   private List<JsonEntry> dependenciesEntries(PackageJsonDependencies dependencies) {
@@ -179,6 +187,9 @@ class FileSystemPackageJsonHandler {
     private final String blockValue;
 
     private JsonAction(JsonActionBuilder builder) {
+      Assert.notNull("action", builder.action);
+      Assert.notNull("entries", builder.entries);
+
       blockName = builder.blockName;
       jsonContent = builder.jsonContent;
       indentation = builder.indentation;
@@ -196,13 +207,11 @@ class FileSystemPackageJsonHandler {
     }
 
     public String handle() {
-      Assert.notNull("action", action);
-
       if (blockValue != null) {
         return appendNewRootEntry(jsonContent);
       }
 
-      if (entries == null || entries.isEmpty()) {
+      if (entries.isEmpty()) {
         return jsonContent;
       }
 
@@ -312,7 +321,7 @@ class FileSystemPackageJsonHandler {
       private String blockName;
       private String jsonContent;
       private Indentation indentation;
-      private Collection<JsonEntry> entries;
+      private Collection<JsonEntry> entries = List.of();
       private final JsonActionType action;
       private String blockValue;
 
@@ -339,7 +348,7 @@ class FileSystemPackageJsonHandler {
       }
 
       private JsonActionBuilder entries(Collection<JsonEntry> entries) {
-        this.entries = entries;
+        this.entries = JHipsterCollections.immutable(entries);
 
         return this;
       }


Test patch:
diff --git a/src/test/java/tech/jhipster/lite/module/infrastructure/secondary/FileSystemPackageJsonHandlerTest.java b/src/test/java/tech/jhipster/lite/module/infrastructure/secondary/FileSystemPackageJsonHandlerTest.java
index cacc63655..9b7783406 100644
--- a/src/test/java/tech/jhipster/lite/module/infrastructure/secondary/FileSystemPackageJsonHandlerTest.java
+++ b/src/test/java/tech/jhipster/lite/module/infrastructure/secondary/FileSystemPackageJsonHandlerTest.java
@@ -5,6 +5,8 @@ import static org.mockito.Mockito.*;
 import static tech.jhipster.lite.module.domain.JHipsterModule.*;
 import static tech.jhipster.lite.module.domain.JHipsterModulesFixture.emptyModuleBuilder;
 import static tech.jhipster.lite.module.domain.JHipsterModulesFixture.emptyModuleContext;
+import static tech.jhipster.lite.module.domain.packagejson.NodeModuleFormat.COMMONJS;
+import static tech.jhipster.lite.module.domain.packagejson.NodeModuleFormat.MODULE;
 
 import java.io.IOException;
 import java.nio.file.Files;
@@ -103,6 +105,55 @@ class FileSystemPackageJsonHandlerTest {
     );
   }
 
+  @Nested
+  class Type {
+
+    @Test
+    void shouldAddTypeToPackageJsonWithoutType() {
+      JHipsterProjectFolder folder = projectWithPackageJson("src/test/resources/projects/empty-node/package.json");
+
+      packageJson.handle(Indentation.DEFAULT, folder, packageJson(p -> p.type(MODULE)), emptyModuleContext());
+
+      assertPackageJsonContent(
+        folder,
+        """
+          "type": "module"
+        }
+        """
+      );
+    }
+
+    @Test
+    void shouldReplaceExistingType() {
+      JHipsterProjectFolder folder = projectWithPackageJson("src/test/resources/projects/node/package.json");
+
+      packageJson.handle(Indentation.DEFAULT, folder, packageJson(p -> p.type(COMMONJS)), emptyModuleContext());
+
+      assertPackageJsonContent(
+        folder,
+        """
+          "type": "commonjs"
+        }
+        """
+      );
+    }
+
+    @Test
+    void deprecatedApiShouldWork() {
+      JHipsterProjectFolder folder = projectWithPackageJson("src/test/resources/projects/empty-node/package.json");
+
+      packageJson.handle(Indentation.DEFAULT, folder, packageJson(p -> p.addType("module")), emptyModuleContext());
+
+      assertPackageJsonContent(
+        folder,
+        """
+          "type": "module"
+        }
+        """
+      );
+    }
+  }
+
   @Nested
   class Scripts {
 
diff --git a/src/test/resources/projects/node/package.json b/src/test/resources/projects/node/package.json
index 336c9b415..a0eaef2fe 100644
--- a/src/test/resources/projects/node/package.json
+++ b/src/test/resources/projects/node/package.json
@@ -4,6 +4,7 @@
   "private": true,
   "description": "JHipster Project",
   "license": "UNLICENSED",
+  "type": "module",
   "scripts": {
     "build": "vue-tsc -p tsconfig-build.json --noEmit && vite build --emptyOutDir"
   },