Repository: jhipster/jhipster-lite (java)

Issue:
Add missing gradle frontend server plugin
**Prerequisites**

- [x] #9744
- [x] https://github.com/jhipster/jhipster-lite/issues/9788
- [x] https://github.com/jhipster/jhipster-lite/issues/9813
- [x]  https://github.com/jhipster/jhipster-lite/issues/9830

[This sample](https://github.com/renanfranca/sample-frontend-gradle/pull/2/files) implemented the following features which is going to be handle by this issue:

- install the npm inside the `build` folder
- `npm install`
- `npm run build` from the `package.json`
- `npm run test` from the `package.json`
- add the frontend build project inside the .jar

To test the sample:

1. `./gradlew clean build --no-daemon --info` (will generate the .jar at `build/libs` folder)
2. `java -jar build/libs/jhipster-sample-application-0.0.1-SNAPSHOT.jar` (access the frontend at http://localhost:8080)
3. `docker compose -f src/main/docker/sonar.yml up -d` and `./gradlew clean build sonar --info` (all frontend tests will be at sonar with 100% coverage)

Sharing what I learned after a long time dedicated:
- Angular doesn't work with the Maven front-end plugin or with Gradle **edit: when run the generated .jar.**. [This was fixed at this issue](https://github.com/jhipster/jhipster-lite/issues/9830)
- Vue works with the Maven front-end plugin; it adds the `target/classes/static` folder with Vue files directly to the .jar without extra configuration.
- Vue on Gradle doesn't automatically add the static folder to the .jar. [You need to explicitly define this in `build.gradle.kts`, and also make sure that `npm run build` runs before the `tasks.bootJar` task](https://github.com/renanfranca/sample-frontend-gradle/pull/2/files#diff-c0dfa6bc7a8685217f70a860145fbdf416d449eaff052fa28352c5cec1a98c06R103-R108), which is responsible for generating the .jar.

Pull request:
add missing frontend server plugin
Fix #9743 

Patch:
diff --git a/src/main/java/tech/jhipster/lite/generator/server/javatool/frontendmaven/application/FrontendJavaBuildToolApplicationService.java b/src/main/java/tech/jhipster/lite/generator/server/javatool/frontendmaven/application/FrontendJavaBuildToolApplicationService.java
new file mode 100644
index 000000000..2a9dcf9a2
--- /dev/null
+++ b/src/main/java/tech/jhipster/lite/generator/server/javatool/frontendmaven/application/FrontendJavaBuildToolApplicationService.java
@@ -0,0 +1,25 @@
+package tech.jhipster.lite.generator.server.javatool.frontendmaven.application;
+
+import org.springframework.stereotype.Service;
+import tech.jhipster.lite.generator.server.javatool.frontendmaven.domain.FrontendJavaBuildToolModuleFactory;
+import tech.jhipster.lite.module.domain.JHipsterModule;
+import tech.jhipster.lite.module.domain.npm.NpmVersions;
+import tech.jhipster.lite.module.domain.properties.JHipsterModuleProperties;
+
+@Service
+public class FrontendJavaBuildToolApplicationService {
+
+  private final FrontendJavaBuildToolModuleFactory factory;
+
+  public FrontendJavaBuildToolApplicationService(NpmVersions npmVersions) {
+    factory = new FrontendJavaBuildToolModuleFactory(npmVersions);
+  }
+
+  public JHipsterModule buildFrontendMavenModule(JHipsterModuleProperties properties) {
+    return factory.buildFrontendMavenModule(properties);
+  }
+
+  public JHipsterModule buildFrontendGradleModule(JHipsterModuleProperties properties) {
+    return factory.buildFrontendGradleModule(properties);
+  }
+}
diff --git a/src/main/java/tech/jhipster/lite/generator/server/javatool/frontendmaven/application/FrontendMavenApplicationService.java b/src/main/java/tech/jhipster/lite/generator/server/javatool/frontendmaven/application/FrontendMavenApplicationService.java
deleted file mode 100644
index 1ab8b6894..000000000
--- a/src/main/java/tech/jhipster/lite/generator/server/javatool/frontendmaven/application/FrontendMavenApplicationService.java
+++ /dev/null
@@ -1,21 +0,0 @@
-package tech.jhipster.lite.generator.server.javatool.frontendmaven.application;
-
-import org.springframework.stereotype.Service;
-import tech.jhipster.lite.generator.server.javatool.frontendmaven.domain.FrontendMavenModuleFactory;
-import tech.jhipster.lite.module.domain.JHipsterModule;
-import tech.jhipster.lite.module.domain.npm.NpmVersions;
-import tech.jhipster.lite.module.domain.properties.JHipsterModuleProperties;
-
-@Service
-public class FrontendMavenApplicationService {
-
-  private final FrontendMavenModuleFactory factory;
-
-  public FrontendMavenApplicationService(NpmVersions npmVersions) {
-    factory = new FrontendMavenModuleFactory(npmVersions);
-  }
-
-  public JHipsterModule buildModule(JHipsterModuleProperties properties) {
-    return factory.buildModule(properties);
-  }
-}
diff --git a/src/main/java/tech/jhipster/lite/generator/server/javatool/frontendmaven/domain/FrontendMavenModuleFactory.java b/src/main/java/tech/jhipster/lite/generator/server/javatool/frontendmaven/domain/FrontendJavaBuildToolModuleFactory.java
similarity index 69%
rename from src/main/java/tech/jhipster/lite/generator/server/javatool/frontendmaven/domain/FrontendMavenModuleFactory.java
rename to src/main/java/tech/jhipster/lite/generator/server/javatool/frontendmaven/domain/FrontendJavaBuildToolModuleFactory.java
index 58128d1b9..0ff1c99ae 100644
--- a/src/main/java/tech/jhipster/lite/generator/server/javatool/frontendmaven/domain/FrontendMavenModuleFactory.java
+++ b/src/main/java/tech/jhipster/lite/generator/server/javatool/frontendmaven/domain/FrontendJavaBuildToolModuleFactory.java
@@ -6,13 +6,14 @@ import static tech.jhipster.lite.module.domain.mavenplugin.MavenBuildPhase.*;
 import tech.jhipster.lite.module.domain.JHipsterModule;
 import tech.jhipster.lite.module.domain.file.JHipsterDestination;
 import tech.jhipster.lite.module.domain.file.JHipsterSource;
+import tech.jhipster.lite.module.domain.gradleplugin.GradleMainBuildPlugin;
 import tech.jhipster.lite.module.domain.mavenplugin.MavenPlugin;
 import tech.jhipster.lite.module.domain.npm.NpmVersionSource;
 import tech.jhipster.lite.module.domain.npm.NpmVersions;
 import tech.jhipster.lite.module.domain.properties.JHipsterModuleProperties;
 import tech.jhipster.lite.shared.error.domain.Assert;
 
-public class FrontendMavenModuleFactory {
+public class FrontendJavaBuildToolModuleFactory {
 
   private static final String PACKAGE_INFO = "package-info.java";
 
@@ -23,18 +24,13 @@ public class FrontendMavenModuleFactory {
 
   private final NpmVersions npmVersions;
 
-  public FrontendMavenModuleFactory(NpmVersions npmVersions) {
+  public FrontendJavaBuildToolModuleFactory(NpmVersions npmVersions) {
     this.npmVersions = npmVersions;
   }
 
-  public JHipsterModule buildModule(JHipsterModuleProperties properties) {
-    Assert.notNull("properties", properties);
-
-    String packagePath = properties.packagePath();
-
-    JHipsterDestination mainDestination = toSrcMainJava().append(packagePath);
+  public JHipsterModule buildFrontendMavenModule(JHipsterModuleProperties properties) {
     //@formatter:off
-    return moduleBuilder(properties)
+    return commonModuleFiles(properties)
       .javaBuildProperties()
         .set(buildPropertyKey("node.version"), buildPropertyValue("v" + npmVersions.nodeVersion().get()))
         .set(buildPropertyKey("npm.version"), buildPropertyValue(npmVersions.get("npm", NpmVersionSource.COMMON).get()))
@@ -44,13 +40,6 @@ public class FrontendMavenModuleFactory {
         .plugin(antrunPlugin())
         .plugin(frontendMavenPlugin())
         .and()
-      .files()
-        .add(
-          SOURCE.template("RedirectionResource.java"),
-          mainDestination.append(REDIRECTION_PRIMARY).append("RedirectionResource.java")
-        )
-        .add(SOURCE.template(PACKAGE_INFO), mainDestination.append(REDIRECTION).append(PACKAGE_INFO))
-        .and()
       .build();
     //@formatter:on
   }
@@ -174,4 +163,90 @@ public class FrontendMavenModuleFactory {
       )
       .build();
   }
+
+  public JHipsterModule buildFrontendGradleModule(JHipsterModuleProperties properties) {
+    //@formatter:off
+    return commonModuleFiles(properties)
+      .javaBuildProperties()
+        .set(buildPropertyKey("node.version.value"), buildPropertyValue(npmVersions.nodeVersion().get()))
+        .set(buildPropertyKey("npm.version.value"), buildPropertyValue(npmVersions.get("npm", NpmVersionSource.COMMON).get()))
+        .and()
+      .gradlePlugins()
+        .plugin(frontendGradlePlugin())
+        .and()
+      .gradleConfigurations()
+        .addTasksTestInstruction(
+          """
+          dependsOn("testNpm")\
+          """
+        )
+        .and()
+      .build();
+    //@formatter:on
+  }
+
+  private static JHipsterModuleBuilder commonModuleFiles(JHipsterModuleProperties properties) {
+    Assert.notNull("properties", properties);
+
+    String packagePath = properties.packagePath();
+
+    JHipsterDestination mainDestination = toSrcMainJava().append(packagePath);
+    //@formatter:off
+    return moduleBuilder(properties)
+      .files()
+        .add(
+          SOURCE.template("RedirectionResource.java"),
+          mainDestination.append(REDIRECTION_PRIMARY).append("RedirectionResource.java")
+        )
+        .add(SOURCE.template(PACKAGE_INFO), mainDestination.append(REDIRECTION).append(PACKAGE_INFO))
+        .and();
+    //@formatter:on
+  }
+
+  private GradleMainBuildPlugin frontendGradlePlugin() {
+    return gradleCommunityPlugin()
+      .id("com.github.node-gradle.node")
+      .pluginSlug("node-gradle")
+      .versionSlug("node-gradle")
+      .withBuildGradleImport("com.github.gradle.node.npm.task.NpmTask")
+      .configuration(
+        """
+        node {
+          download.set(true)
+          version.set(nodeVersionValue)
+          npmVersion.set(npmVersionValue)
+          workDir.set(file(layout.buildDirectory))
+          npmWorkDir.set(file(layout.buildDirectory))
+        }
+
+        val buildTaskUsingNpm = tasks.register<NpmTask>("buildNpm") {
+          description = "Build the frontend project using NPM"
+          group = "Build"
+          dependsOn("npmInstall")
+          npmCommand.set(listOf("run", "build"))
+          environment.set(mapOf("APP_VERSION" to project.version.toString()))
+        }
+
+        val testTaskUsingNpm = tasks.register<NpmTask>("testNpm") {
+          description = "Test the frontend project using NPM"
+          group = "verification"
+          dependsOn("npmInstall", "buildNpm")
+          npmCommand.set(listOf("run", "test:coverage"))
+          ignoreExitValue.set(false)
+          workingDir.set(projectDir)
+          execOverrides {
+            standardOutput = System.out
+          }
+        }
+
+        tasks.bootJar {
+          dependsOn("buildNpm")
+          from("build/classes/static") {
+              into("BOOT-INF/classes/static")
+          }
+        }
+        """
+      )
+      .build();
+  }
 }
diff --git a/src/main/java/tech/jhipster/lite/generator/server/javatool/frontendmaven/infrastructure/primary/FrontendJavaBuildToolModuleConfiguration.java b/src/main/java/tech/jhipster/lite/generator/server/javatool/frontendmaven/infrastructure/primary/FrontendJavaBuildToolModuleConfiguration.java
new file mode 100644
index 000000000..f68a7c040
--- /dev/null
+++ b/src/main/java/tech/jhipster/lite/generator/server/javatool/frontendmaven/infrastructure/primary/FrontendJavaBuildToolModuleConfiguration.java
@@ -0,0 +1,51 @@
+package tech.jhipster.lite.generator.server.javatool.frontendmaven.infrastructure.primary;
+
+import static tech.jhipster.lite.generator.slug.domain.JHLiteFeatureSlug.*;
+import static tech.jhipster.lite.generator.slug.domain.JHLiteModuleSlug.*;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import tech.jhipster.lite.generator.server.javatool.frontendmaven.application.FrontendJavaBuildToolApplicationService;
+import tech.jhipster.lite.module.domain.resource.JHipsterModuleOrganization;
+import tech.jhipster.lite.module.domain.resource.JHipsterModulePropertiesDefinition;
+import tech.jhipster.lite.module.domain.resource.JHipsterModuleResource;
+
+@Configuration
+class FrontendJavaBuildToolModuleConfiguration {
+
+  @Bean
+  JHipsterModuleResource frontendMavenModule(FrontendJavaBuildToolApplicationService frontendJavaBuildTool) {
+    return JHipsterModuleResource.builder()
+      .slug(FRONTEND_MAVEN_PLUGIN)
+      .propertiesDefinition(JHipsterModulePropertiesDefinition.builder().addBasePackage().addProjectBaseName().addIndentation().build())
+      .apiDoc("Frontend Maven Plugin", "Add Frontend Maven Plugin")
+      .organization(
+        JHipsterModuleOrganization.builder()
+          .feature(FRONTEND_JAVA_BUILD_TOOL_PLUGIN)
+          .addDependency(SPRING_SERVER)
+          .addDependency(CLIENT_CORE)
+          .addDependency(MAVEN_JAVA)
+          .build()
+      )
+      .tags("server", "tools")
+      .factory(frontendJavaBuildTool::buildFrontendMavenModule);
+  }
+
+  @Bean
+  JHipsterModuleResource frontendGradleModule(FrontendJavaBuildToolApplicationService frontendJavaBuildTool) {
+    return JHipsterModuleResource.builder()
+      .slug(NODE_GRADLE_PLUGIN)
+      .propertiesDefinition(JHipsterModulePropertiesDefinition.builder().addBasePackage().addProjectBaseName().addIndentation().build())
+      .apiDoc("Frontend Gradle Plugin", "Add node-gradle plugin for building frontend with Gradle")
+      .organization(
+        JHipsterModuleOrganization.builder()
+          .feature(FRONTEND_JAVA_BUILD_TOOL_PLUGIN)
+          .addDependency(SPRING_SERVER)
+          .addDependency(CLIENT_CORE)
+          .addDependency(GRADLE_JAVA)
+          .build()
+      )
+      .tags("server", "tools")
+      .factory(frontendJavaBuildTool::buildFrontendGradleModule);
+  }
+}
diff --git a/src/main/java/tech/jhipster/lite/generator/server/javatool/frontendmaven/infrastructure/primary/FrontendMavenModuleConfiguration.java b/src/main/java/tech/jhipster/lite/generator/server/javatool/frontendmaven/infrastructure/primary/FrontendMavenModuleConfiguration.java
deleted file mode 100644
index 4e6cb1f46..000000000
--- a/src/main/java/tech/jhipster/lite/generator/server/javatool/frontendmaven/infrastructure/primary/FrontendMavenModuleConfiguration.java
+++ /dev/null
@@ -1,26 +0,0 @@
-package tech.jhipster.lite.generator.server.javatool.frontendmaven.infrastructure.primary;
-
-import static tech.jhipster.lite.generator.slug.domain.JHLiteFeatureSlug.*;
-import static tech.jhipster.lite.generator.slug.domain.JHLiteModuleSlug.*;
-
-import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.Configuration;
-import tech.jhipster.lite.generator.server.javatool.frontendmaven.application.FrontendMavenApplicationService;
-import tech.jhipster.lite.module.domain.resource.JHipsterModuleOrganization;
-import tech.jhipster.lite.module.domain.resource.JHipsterModulePropertiesDefinition;
-import tech.jhipster.lite.module.domain.resource.JHipsterModuleResource;
-
-@Configuration
-class FrontendMavenModuleConfiguration {
-
-  @Bean
-  JHipsterModuleResource frontendMavenModule(FrontendMavenApplicationService frontendMaven) {
-    return JHipsterModuleResource.builder()
-      .slug(FRONTEND_MAVEN_PLUGIN)
-      .propertiesDefinition(JHipsterModulePropertiesDefinition.builder().addBasePackage().addProjectBaseName().addIndentation().build())
-      .apiDoc("Frontend Maven Plugin", "Add Frontend Maven Plugin")
-      .organization(JHipsterModuleOrganization.builder().addDependency(SPRING_SERVER).addDependency(CLIENT_CORE).build())
-      .tags("server", "tools")
-      .factory(frontendMaven::buildModule);
-  }
-}
diff --git a/src/main/java/tech/jhipster/lite/generator/slug/domain/JHLiteFeatureSlug.java b/src/main/java/tech/jhipster/lite/generator/slug/domain/JHLiteFeatureSlug.java
index 9aed9f79d..1ec688e2f 100644
--- a/src/main/java/tech/jhipster/lite/generator/slug/domain/JHLiteFeatureSlug.java
+++ b/src/main/java/tech/jhipster/lite/generator/slug/domain/JHLiteFeatureSlug.java
@@ -13,6 +13,7 @@ public enum JHLiteFeatureSlug implements JHipsterFeatureSlugFactory {
   DATABASE_MIGRATION("database-migration"),
   DOCKERFILE("dockerfile"),
   FRONT_BROWSER_TEST("front-browser-test"),
+  FRONTEND_JAVA_BUILD_TOOL_PLUGIN("frontend-java-build-tool-plugin"),
   GITHUB_ACTIONS("github-actions"),
   GITLAB_CI("gitlab-ci"),
   DEPENDENCIES_UPDATES("dependencies-updates"),
diff --git a/src/main/java/tech/jhipster/lite/generator/slug/domain/JHLiteModuleSlug.java b/src/main/java/tech/jhipster/lite/generator/slug/domain/JHLiteModuleSlug.java
index bcef8b0d0..3cae24ec0 100644
--- a/src/main/java/tech/jhipster/lite/generator/slug/domain/JHLiteModuleSlug.java
+++ b/src/main/java/tech/jhipster/lite/generator/slug/domain/JHLiteModuleSlug.java
@@ -34,6 +34,7 @@ public enum JHLiteModuleSlug implements JHipsterModuleSlugFactory {
   FLYWAY("flyway"),
   FLYWAY_MYSQL("flyway-mysql"),
   FRONTEND_MAVEN_PLUGIN("frontend-maven-plugin"),
+  NODE_GRADLE_PLUGIN("node-gradle-plugin"),
   GATEWAY("gateway"),
   GIT_INFORMATION("git-information"),
   GITHUB_ACTIONS_MAVEN("github-actions-maven"),
diff --git a/src/main/resources/generator/buildtool/gradle/build.gradle.kts.mustache b/src/main/resources/generator/buildtool/gradle/build.gradle.kts.mustache
index 4a602927e..4212b1de5 100644
--- a/src/main/resources/generator/buildtool/gradle/build.gradle.kts.mustache
+++ b/src/main/resources/generator/buildtool/gradle/build.gradle.kts.mustache
@@ -5,6 +5,8 @@ plugins {
   // jhipster-needle-gradle-plugins
 }
 
+// jhipster-needle-gradle-properties
+
 java {
   toolchain {
     languageVersion = JavaLanguageVersion.of({{javaVersion}})
@@ -20,8 +22,6 @@ repositories {
 group = "{{packageName}}"
 version = "0.0.1-SNAPSHOT"
 
-// jhipster-needle-gradle-properties
-
 val profiles = (project.findProperty("profiles") as String? ?: "")
   .split(",")
   .map { it.trim() }
diff --git a/src/main/resources/generator/buildtool/gradle/buildSrc/src/main/kotlin/profile.gradle.kts.template b/src/main/resources/generator/buildtool/gradle/buildSrc/src/main/kotlin/profile.gradle.kts.template
index 4f101c808..8a158b360 100644
--- a/src/main/resources/generator/buildtool/gradle/buildSrc/src/main/kotlin/profile.gradle.kts.template
+++ b/src/main/resources/generator/buildtool/gradle/buildSrc/src/main/kotlin/profile.gradle.kts.template
@@ -5,6 +5,8 @@ plugins {
   // jhipster-needle-gradle-plugins
 }
 
+// jhipster-needle-gradle-properties
+
 // jhipster-needle-gradle-plugins-configurations
 
 // Workaround to access the `libs` object in precompiled script plugin
@@ -17,5 +19,3 @@ dependencies {
   // jhipster-needle-gradle-runtime-dependencies
   // jhipster-needle-gradle-test-dependencies
 }
-
-// jhipster-needle-gradle-properties
diff --git a/src/main/resources/generator/dependencies/gradle/libs.versions.toml b/src/main/resources/generator/dependencies/gradle/libs.versions.toml
index d72df007b..d0c95dafa 100644
--- a/src/main/resources/generator/dependencies/gradle/libs.versions.toml
+++ b/src/main/resources/generator/dependencies/gradle/libs.versions.toml
@@ -5,6 +5,7 @@ git-properties = "2.4.2"
 protobuf-plugin = "0.9.4"
 jacoco = "0.8.11"
 sonarqube = "4.4.1.3373"
+node-gradle = "7.0.2"
 
 [libraries]
 [libraries.checkstyle]


Test patch:
diff --git a/src/test/features/server/javatool/frontend-java-build-tool.feature b/src/test/features/server/javatool/frontend-java-build-tool.feature
new file mode 100644
index 000000000..cf8c90d56
--- /dev/null
+++ b/src/test/features/server/javatool/frontend-java-build-tool.feature
@@ -0,0 +1,15 @@
+Feature: Frontend server module
+
+  Scenario: Should apply frontend server maven module
+    When I apply "frontend-maven-plugin" module to default project with maven file
+      | packageName | tech.jhipster.chips |
+      | baseName    | jhipster            |
+    Then I should have files in "src/main/java/tech/jhipster/chips/wire/frontend/infrastructure/primary"
+      | RedirectionResource.java |
+
+  Scenario: Should apply frontend server gradle module
+    When I apply "node-gradle-plugin" module to default project with gradle build
+      | packageName | tech.jhipster.chips |
+      | baseName    | jhipster            |
+    Then I should have files in "src/main/java/tech/jhipster/chips/wire/frontend/infrastructure/primary"
+      | RedirectionResource.java |
diff --git a/src/test/features/server/javatool/frontend-maven.feature b/src/test/features/server/javatool/frontend-maven.feature
deleted file mode 100644
index 8969ca2f1..000000000
--- a/src/test/features/server/javatool/frontend-maven.feature
+++ /dev/null
@@ -1,8 +0,0 @@
-Feature: Frontend maven module
-
-  Scenario: Should apply frontend maven module
-    When I apply "frontend-maven-plugin" module to default project with maven file
-      | packageName | tech.jhipster.chips |
-      | baseName    | jhipster            |
-    Then I should have files in "src/main/java/tech/jhipster/chips/wire/frontend/infrastructure/primary"
-      | RedirectionResource.java |
diff --git a/src/test/java/tech/jhipster/lite/generator/server/javatool/frontendmaven/domain/FrontendMavenModuleFactoryTest.java b/src/test/java/tech/jhipster/lite/generator/server/javatool/frontendmaven/domain/FrontendJavaBuildToolModuleFactoryTest.java
similarity index 70%
rename from src/test/java/tech/jhipster/lite/generator/server/javatool/frontendmaven/domain/FrontendMavenModuleFactoryTest.java
rename to src/test/java/tech/jhipster/lite/generator/server/javatool/frontendmaven/domain/FrontendJavaBuildToolModuleFactoryTest.java
index b932a4890..9f3384734 100644
--- a/src/test/java/tech/jhipster/lite/generator/server/javatool/frontendmaven/domain/FrontendMavenModuleFactoryTest.java
+++ b/src/test/java/tech/jhipster/lite/generator/server/javatool/frontendmaven/domain/FrontendJavaBuildToolModuleFactoryTest.java
@@ -3,6 +3,7 @@ package tech.jhipster.lite.generator.server.javatool.frontendmaven.domain;
 import static org.mockito.Mockito.*;
 import static tech.jhipster.lite.module.infrastructure.secondary.JHipsterModulesAssertions.*;
 
+import org.jetbrains.annotations.NotNull;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
 import org.mockito.InjectMocks;
@@ -19,25 +20,19 @@ import tech.jhipster.lite.module.domain.properties.JHipsterModuleProperties;
 
 @UnitTest
 @ExtendWith(MockitoExtension.class)
-class FrontendMavenModuleFactoryTest {
+class FrontendJavaBuildToolModuleFactoryTest {
 
   @Mock
   private NpmVersions npmVersions;
 
   @InjectMocks
-  private FrontendMavenModuleFactory factory;
+  private FrontendJavaBuildToolModuleFactory factory;
 
   @Test
-  void shouldBuildModule() {
-    JHipsterModuleProperties properties = JHipsterModulesFixture.propertiesBuilder(TestFileUtils.tmpDirForTest())
-      .basePackage("com.jhipster.test")
-      .projectBaseName("myapp")
-      .build();
-
-    when(npmVersions.get("npm", NpmVersionSource.COMMON)).thenReturn(new NpmPackageVersion("4.0.0"));
-    when(npmVersions.nodeVersion()).thenReturn(new NpmPackageVersion("16.0.0"));
+  void shouldBuildFrontendMavenModule() {
+    mockNpmVersion();
 
-    JHipsterModule module = factory.buildModule(properties);
+    JHipsterModule module = factory.buildFrontendMavenModule(getProperties());
 
     assertThatModuleWithFiles(module, pomFile())
       .hasFile("pom.xml")
@@ -184,4 +179,110 @@ class FrontendMavenModuleFactoryTest {
         "package-info.java"
       );
   }
+
+  @Test
+  void shouldBuildFrontendGradleModule() {
+    mockNpmVersion();
+
+    JHipsterModule module = factory.buildFrontendGradleModule(getProperties());
+
+    assertThatModuleWithFiles(module, gradleBuildFile(), gradleLibsVersionFile())
+      .hasFile("gradle/libs.versions.toml")
+      .containing("node-gradle = \"")
+      .containing(
+        """
+        \t[plugins.node-gradle]
+        \t\tid = "com.github.node-gradle.node"
+
+        \t\t[plugins.node-gradle.version]
+        \t\t\tref = "node-gradle"
+        """
+      )
+      .and()
+      .hasFile("build.gradle.kts")
+      .containing(
+        """
+        import com.github.gradle.node.npm.task.NpmTask
+        // jhipster-needle-gradle-imports
+        """
+      )
+      .containing(
+        """
+          alias(libs.plugins.node.gradle)
+          // jhipster-needle-gradle-plugins
+        """
+      )
+      .containing(
+        """
+        val nodeVersionValue by extra("16.0.0")
+        val npmVersionValue by extra("4.0.0")
+        // jhipster-needle-gradle-properties
+        """
+      )
+      .containing(
+        """
+        node {
+          download.set(true)
+          version.set(nodeVersionValue)
+          npmVersion.set(npmVersionValue)
+          workDir.set(file(layout.buildDirectory))
+          npmWorkDir.set(file(layout.buildDirectory))
+        }
+        """
+      )
+      .containing(
+        """
+        val buildTaskUsingNpm = tasks.register<NpmTask>("buildNpm") {
+          description = "Build the frontend project using NPM"
+          group = "Build"
+          dependsOn("npmInstall")
+          npmCommand.set(listOf("run", "build"))
+          environment.set(mapOf("APP_VERSION" to project.version.toString()))
+        }
+        """
+      )
+      .containing(
+        """
+        val testTaskUsingNpm = tasks.register<NpmTask>("testNpm") {
+          description = "Test the frontend project using NPM"
+          group = "verification"
+          dependsOn("npmInstall", "buildNpm")
+          npmCommand.set(listOf("run", "test:coverage"))
+          ignoreExitValue.set(false)
+          workingDir.set(projectDir)
+          execOverrides {
+            standardOutput = System.out
+          }
+        }
+        """
+      )
+      .containing(
+        """
+        tasks.bootJar {
+          dependsOn("buildNpm")
+          from("build/classes/static") {
+              into("BOOT-INF/classes/static")
+          }
+        }
+        """
+      )
+      .containing(
+        """
+          dependsOn("testNpm")
+          // jhipster-needle-gradle-tasks-test
+        """
+      );
+  }
+
+  private void mockNpmVersion() {
+    when(npmVersions.get("npm", NpmVersionSource.COMMON)).thenReturn(new NpmPackageVersion("4.0.0"));
+    when(npmVersions.nodeVersion()).thenReturn(new NpmPackageVersion("16.0.0"));
+  }
+
+  private static @NotNull JHipsterModuleProperties getProperties() {
+    return JHipsterModulesFixture.propertiesBuilder(TestFileUtils.tmpDirForTest())
+      .basePackage("com.jhipster.test")
+      .projectBaseName("myapp")
+      .build();
+  }
 }
diff --git a/src/test/resources/projects/empty-gradle/build.gradle.kts b/src/test/resources/projects/empty-gradle/build.gradle.kts
index 21d581bf1..53ea9b616 100644
--- a/src/test/resources/projects/empty-gradle/build.gradle.kts
+++ b/src/test/resources/projects/empty-gradle/build.gradle.kts
@@ -5,6 +5,8 @@ plugins {
   // jhipster-needle-gradle-plugins
 }
 
+// jhipster-needle-gradle-properties
+
 java {
   toolchain {
     languageVersion = JavaLanguageVersion.of(21)
@@ -21,8 +23,6 @@ repositories {
 group = "tech.jhipster.chips"
 version = "0.0.1-SNAPSHOT"
 
-// jhipster-needle-gradle-properties
-
 val profiles = (project.findProperty("profiles") as String? ?: "")
   .split(",")
   .map { it.trim() }
diff --git a/src/test/resources/projects/gradle-with-local-profile-and-properties/buildSrc/src/main/kotlin/profile-local.gradle.kts b/src/test/resources/projects/gradle-with-local-profile-and-properties/buildSrc/src/main/kotlin/profile-local.gradle.kts
index c957a9245..8cf742087 100644
--- a/src/test/resources/projects/gradle-with-local-profile-and-properties/buildSrc/src/main/kotlin/profile-local.gradle.kts
+++ b/src/test/resources/projects/gradle-with-local-profile-and-properties/buildSrc/src/main/kotlin/profile-local.gradle.kts
@@ -3,6 +3,9 @@ plugins {
   // jhipster-needle-gradle-plugins
 }
 
+val javaVersion by extra("21")
+// jhipster-needle-gradle-properties
+
 dependencies {
   // Access to the `libs` object doesn't work in precompiled script plugin
   // https://github.com/gradle/gradle/issues/15383
@@ -13,6 +16,3 @@ dependencies {
   // jhipster-needle-gradle-runtime-dependencies
   // jhipster-needle-gradle-test-dependencies
 }
-
-val javaVersion by extra("21")
-// jhipster-needle-gradle-properties
diff --git a/src/test/resources/projects/gradle-with-local-profile/buildSrc/src/main/kotlin/profile-local.gradle.kts b/src/test/resources/projects/gradle-with-local-profile/buildSrc/src/main/kotlin/profile-local.gradle.kts
index 4f101c808..8a158b360 100644
--- a/src/test/resources/projects/gradle-with-local-profile/buildSrc/src/main/kotlin/profile-local.gradle.kts
+++ b/src/test/resources/projects/gradle-with-local-profile/buildSrc/src/main/kotlin/profile-local.gradle.kts
@@ -5,6 +5,8 @@ plugins {
   // jhipster-needle-gradle-plugins
 }
 
+// jhipster-needle-gradle-properties
+
 // jhipster-needle-gradle-plugins-configurations
 
 // Workaround to access the `libs` object in precompiled script plugin
@@ -17,5 +19,3 @@ dependencies {
   // jhipster-needle-gradle-runtime-dependencies
   // jhipster-needle-gradle-test-dependencies
 }
-
-// jhipster-needle-gradle-properties
diff --git a/src/test/resources/projects/gradle-with-properties/build.gradle.kts b/src/test/resources/projects/gradle-with-properties/build.gradle.kts
index 121c78aa2..464c7cbe4 100644
--- a/src/test/resources/projects/gradle-with-properties/build.gradle.kts
+++ b/src/test/resources/projects/gradle-with-properties/build.gradle.kts
@@ -3,6 +3,9 @@ plugins {
   // jhipster-needle-gradle-plugins
 }
 
+val javaVersion by extra("21")
+// jhipster-needle-gradle-properties
+
 java {
   toolchain {
     languageVersion = JavaLanguageVersion.of(21)
@@ -19,9 +22,6 @@ repositories {
 group = "tech.jhipster.chips"
 version = "0.0.1-SNAPSHOT"
 
-val javaVersion by extra("21")
-// jhipster-needle-gradle-properties
-
 val profiles = (project.findProperty("profiles") as String? ?: "")
   .split(",")
   .map { it.trim() }
diff --git a/src/test/resources/projects/init-gradle/build.gradle.kts b/src/test/resources/projects/init-gradle/build.gradle.kts
index 665862f57..87594d2d1 100644
--- a/src/test/resources/projects/init-gradle/build.gradle.kts
+++ b/src/test/resources/projects/init-gradle/build.gradle.kts
@@ -5,6 +5,8 @@ plugins {
   // jhipster-needle-gradle-plugins
 }
 
+// jhipster-needle-gradle-properties
+
 java {
   toolchain {
     languageVersion = JavaLanguageVersion.of(21)
@@ -21,8 +23,6 @@ repositories {
 group = "com.jhipster.test"
 version = "0.0.1-SNAPSHOT"
 
-// jhipster-needle-gradle-properties
-
 val profiles = (project.findProperty("profiles") as String? ?: "")
   .split(",")
   .map { it.trim() }
diff --git a/tests-ci/generate.sh b/tests-ci/generate.sh
index 8cbfb8b41..169b6607b 100755
--- a/tests-ci/generate.sh
+++ b/tests-ci/generate.sh
@@ -103,8 +103,7 @@ frontend_server_plugin() {
   if [[ $java_build_tool == 'maven' ]]; then
     applyModules "frontend-maven-plugin"
   else
-    echo -e "No frontend server plugin for $java_build_tool!"
-    exit 1
+    applyModules "node-gradle-plugin"
   fi
 }
 
@@ -132,10 +131,11 @@ elif [[ $application == 'fullstack' ]]; then
 # It should be removed once gradle is fully supported
 elif [[ $application == 'gradleapp' ]]; then
   init_server
-  sonar_back
+  sonar_back_front
   spring_boot_mvc
 
   applyModules \
+    "prettier" \
     "java-memoizers" \
     "java-enums" \
     "spring-boot-local-profile" \
@@ -147,6 +147,8 @@ elif [[ $application == 'gradleapp' ]]; then
     "liquibase"
 
   cucumber_with_jwt
+  frontend_server_plugin
+  applyModules "vue-core"
 
 elif [[ $application == 'fullapp' ]]; then
   init_server