Repository: jhipster/jhipster-lite
Pull request #12940 (2025-07-14 19:06:33)

Add sonar-typescript module
Fix #12735 

qmonmert: @pascalgrimaud is this what you expected?
pascalgrimaud: @qmonmert : I'm reviewing it. Can you resolve conflict plz @qmonmert ?

Patch:
diff --git a/.github/workflows/github-actions.yml b/.github/workflows/github-actions.yml
index 0bf6be07d..ac88cc51e 100644
--- a/.github/workflows/github-actions.yml
+++ b/.github/workflows/github-actions.yml
@@ -470,18 +470,15 @@ jobs:
         if: steps.tests-requirement-check.outputs.execute_tests == 'true'
         working-directory: /tmp/jhlite/${{ matrix.app }}/
         run: |
-          if [ -f 'src/main/docker/sonar.yml' ]; then
-            SONAR_TOKEN=$(docker logs sonar-token)
-          else
-            SONAR_TOKEN=""
-          fi
+          SONAR_TOKEN=$(docker logs sonar-token)
           if [ -f 'mvnw' ]; then
             ./mvnw clean verify -Dsonar.qualitygate.wait=true sonar:sonar -Dsonar.token=$SONAR_TOKEN
           elif [ -f 'gradlew' ]; then
             ./gradlew clean build sonar --no-daemon --info -Dsonar.token=$SONAR_TOKEN
           else
             ${{ matrix.node-package-manager }} install
-            ${{ matrix.node-package-manager }} test
+            ${{ matrix.node-package-manager }} run test:coverage
+            npx @sonar/scan -Dsonar.token=$SONAR_TOKEN
           fi
       - name: 'Test: run linter for ${{ matrix.app }}'
         if: steps.tests-requirement-check.outputs.execute_tests == 'true'
diff --git a/src/main/java/tech/jhipster/lite/generator/typescript/sonar/application/SonarTypescriptApplicationService.java b/src/main/java/tech/jhipster/lite/generator/typescript/sonar/application/SonarTypescriptApplicationService.java
new file mode 100644
index 000000000..de034397b
--- /dev/null
+++ b/src/main/java/tech/jhipster/lite/generator/typescript/sonar/application/SonarTypescriptApplicationService.java
@@ -0,0 +1,21 @@
+package tech.jhipster.lite.generator.typescript.sonar.application;
+
+import org.springframework.stereotype.Service;
+import tech.jhipster.lite.generator.typescript.sonar.domain.SonarTypescriptModuleFactory;
+import tech.jhipster.lite.module.domain.JHipsterModule;
+import tech.jhipster.lite.module.domain.docker.DockerImages;
+import tech.jhipster.lite.module.domain.properties.JHipsterModuleProperties;
+
+@Service
+public class SonarTypescriptApplicationService {
+
+  private final SonarTypescriptModuleFactory sonarTypescript;
+
+  public SonarTypescriptApplicationService(DockerImages dockerImages) {
+    this.sonarTypescript = new SonarTypescriptModuleFactory(dockerImages);
+  }
+
+  public JHipsterModule buildModule(JHipsterModuleProperties project) {
+    return sonarTypescript.buildModule(project);
+  }
+}
diff --git a/src/main/java/tech/jhipster/lite/generator/typescript/sonar/domain/SonarTypescriptModuleFactory.java b/src/main/java/tech/jhipster/lite/generator/typescript/sonar/domain/SonarTypescriptModuleFactory.java
new file mode 100644
index 000000000..fd112d989
--- /dev/null
+++ b/src/main/java/tech/jhipster/lite/generator/typescript/sonar/domain/SonarTypescriptModuleFactory.java
@@ -0,0 +1,50 @@
+package tech.jhipster.lite.generator.typescript.sonar.domain;
+
+import static tech.jhipster.lite.module.domain.JHipsterModule.*;
+import static tech.jhipster.lite.module.domain.nodejs.JHLiteNodePackagesVersionSource.COMMON;
+
+import tech.jhipster.lite.module.domain.JHipsterModule;
+import tech.jhipster.lite.module.domain.docker.DockerImages;
+import tech.jhipster.lite.module.domain.file.JHipsterDestination;
+import tech.jhipster.lite.module.domain.file.JHipsterSource;
+import tech.jhipster.lite.module.domain.properties.JHipsterModuleProperties;
+import tech.jhipster.lite.shared.error.domain.Assert;
+
+public class SonarTypescriptModuleFactory {
+
+  private static final JHipsterSource SOURCE = from("typescript/sonar");
+  private static final JHipsterDestination SONAR_PROPERTIES_DESTINATION = to("sonar-project.properties");
+  private static final String SONARQUBE = "sonarqube";
+
+  private final DockerImages dockerImages;
+
+  public SonarTypescriptModuleFactory(DockerImages dockerImages) {
+    this.dockerImages = dockerImages;
+  }
+
+  public JHipsterModule buildModule(JHipsterModuleProperties properties) {
+    Assert.notNull("properties", properties);
+
+    // @formatter:off
+    return moduleBuilder(properties)
+      .context()
+        .put("sonarqubeDockerImage", dockerImages.get(SONARQUBE).fullName())
+        .and()
+      .packageJson()
+        .addDevDependency(packageName("@sonar/scan"), COMMON)
+        .and()
+      .documentation(documentationTitle("sonar"), SOURCE.template("sonar.md"))
+        .files()
+        .add(SOURCE.template("sonar.yml"), toSrcMainDocker().append("sonar.yml"))
+        .and()
+      .files()
+        .add(SOURCE.append("Dockerfile"), toSrcMainDocker().append("sonar/Dockerfile"))
+        .and()
+      .files()
+        .add(SOURCE.append("sonar_generate_token.sh"), toSrcMainDocker().append("sonar/sonar_generate_token.sh"))
+        .add(SOURCE.template("sonar-project.properties"), SONAR_PROPERTIES_DESTINATION)
+        .and()
+      .build();
+    // @formatter:on
+  }
+}
diff --git a/src/main/java/tech/jhipster/lite/generator/typescript/sonar/infrastructure/primary/SonarTypescriptModuleConfiguration.java b/src/main/java/tech/jhipster/lite/generator/typescript/sonar/infrastructure/primary/SonarTypescriptModuleConfiguration.java
new file mode 100644
index 000000000..c39a59a5c
--- /dev/null
+++ b/src/main/java/tech/jhipster/lite/generator/typescript/sonar/infrastructure/primary/SonarTypescriptModuleConfiguration.java
@@ -0,0 +1,25 @@
+package tech.jhipster.lite.generator.typescript.sonar.infrastructure.primary;
+
+import static tech.jhipster.lite.shared.slug.domain.JHLiteModuleSlug.SONAR_TYPESCRIPT;
+import static tech.jhipster.lite.shared.slug.domain.JHLiteModuleSlug.TYPESCRIPT;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import tech.jhipster.lite.generator.typescript.sonar.application.SonarTypescriptApplicationService;
+import tech.jhipster.lite.module.domain.resource.JHipsterModuleOrganization;
+import tech.jhipster.lite.module.domain.resource.JHipsterModuleResource;
+
+@Configuration
+class SonarTypescriptModuleConfiguration {
+
+  @Bean
+  JHipsterModuleResource sonarTypescriptModule(SonarTypescriptApplicationService sonarTypescript) {
+    return JHipsterModuleResource.builder()
+      .slug(SONAR_TYPESCRIPT)
+      .withoutProperties()
+      .apiDoc("Typescript", "Add Sonar to project")
+      .organization(JHipsterModuleOrganization.builder().addDependency(TYPESCRIPT).build())
+      .tags("typescript")
+      .factory(sonarTypescript::buildModule);
+  }
+}
diff --git a/src/main/java/tech/jhipster/lite/generator/typescript/sonar/package-info.java b/src/main/java/tech/jhipster/lite/generator/typescript/sonar/package-info.java
new file mode 100644
index 000000000..5e7cdbb55
--- /dev/null
+++ b/src/main/java/tech/jhipster/lite/generator/typescript/sonar/package-info.java
@@ -0,0 +1,2 @@
+@tech.jhipster.lite.BusinessContext
+package tech.jhipster.lite.generator.typescript.sonar;
diff --git a/src/main/java/tech/jhipster/lite/shared/slug/domain/JHLiteModuleSlug.java b/src/main/java/tech/jhipster/lite/shared/slug/domain/JHLiteModuleSlug.java
index f796b9835..2d587993a 100644
--- a/src/main/java/tech/jhipster/lite/shared/slug/domain/JHLiteModuleSlug.java
+++ b/src/main/java/tech/jhipster/lite/shared/slug/domain/JHLiteModuleSlug.java
@@ -104,6 +104,7 @@ public enum JHLiteModuleSlug implements JHipsterModuleSlugFactory {
   OPENAPI_CONTRACT("openapi-contract", RANK_B),
   OPENAPI_BACKWARDS_COMPATIBILITY_CHECK("openapi-backwards-compatibility-check", RANK_B),
   OPTIONAL_TYPESCRIPT("optional-typescript", RANK_B),
+  SONAR_TYPESCRIPT("sonar-typescript", RANK_B),
   PAGINATION_DOMAIN("pagination-domain", RANK_A),
   PLAYWRIGHT_COMPONENT_TESTS("playwright-component-tests", RANK_C),
   PLAYWRIGHT_E2E("playwright-e2e", RANK_C),
diff --git a/src/main/resources/generator/dependencies/common/package.json b/src/main/resources/generator/dependencies/common/package.json
index 425aabb81..22ff991ff 100644
--- a/src/main/resources/generator/dependencies/common/package.json
+++ b/src/main/resources/generator/dependencies/common/package.json
@@ -10,6 +10,7 @@
     "@playwright/test": "1.54.1",
     "@prettier/plugin-pug": "3.4.0",
     "@prettier/plugin-xml": "3.4.2",
+    "@sonar/scan": "4.3.0",
     "@tailwindcss/vite": "4.1.11",
     "@tikui/core": "6.3.0",
     "@tsconfig/recommended": "1.0.10",
diff --git a/src/main/resources/generator/init/gitignore b/src/main/resources/generator/init/gitignore
index 2d7245df7..79d54ebc3 100644
--- a/src/main/resources/generator/init/gitignore
+++ b/src/main/resources/generator/init/gitignore
@@ -128,3 +128,8 @@ Desktop.ini
 # ESLint
 ######################
 .eslintcache
+
+######################
+# Sonar
+######################
+.scannerwork/
diff --git a/src/main/resources/generator/typescript/sonar/Dockerfile b/src/main/resources/generator/typescript/sonar/Dockerfile
new file mode 100644
index 000000000..b0de2ed4f
--- /dev/null
+++ b/src/main/resources/generator/typescript/sonar/Dockerfile
@@ -0,0 +1,16 @@
+FROM ubuntu:24.04
+RUN \
+  apt update && \
+  apt --no-install-recommends install -y curl jq && \
+  groupadd jhipster && \
+  useradd jhipster -s /bin/bash -m -g jhipster -G sudo && \
+  echo 'jhipster:jhipster'|chpasswd && \
+  apt clean
+ENV \
+  JHIPSTER_SONAR_URL=http://sonar:9000 \
+  JHIPSTER_SONAR_PASSWORD=JHipsterLite-7471 \
+  JHIPSTER_SONAR_TOKENNAME=jhlitetoken
+COPY sonar/sonar_generate_token.sh /usr/local/bin/sonar_generate_token
+RUN chmod 755 /usr/local/bin/sonar_generate_token
+USER jhipster
+CMD ["sonar_generate_token"]
diff --git a/src/main/resources/generator/typescript/sonar/sonar-project.properties.mustache b/src/main/resources/generator/typescript/sonar/sonar-project.properties.mustache
new file mode 100644
index 000000000..8e330ea1a
--- /dev/null
+++ b/src/main/resources/generator/typescript/sonar/sonar-project.properties.mustache
@@ -0,0 +1,15 @@
+sonar.projectKey={{baseName}}
+sonar.projectName={{projectName}}
+
+sonar.sources=src/main/
+sonar.tests=src/test/
+sonar.host.url=http://localhost:9001
+
+sonar.test.inclusions=src/test/**/*.*, src/main/webapp/**/*.spec.ts
+sonar.testExecutionReportPaths={{projectBuildDirectory}}/test-results/TESTS-results-sonar.xml
+sonar.javascript.lcov.reportPaths={{projectBuildDirectory}}/test-results/lcov.info
+
+sonar.sourceEncoding=UTF-8
+sonar.exclusions=\
+  src/main/webapp/content/**/*.*,\
+  {{projectBuildDirectory}}/classes/static/**/*.*
diff --git a/src/main/resources/generator/typescript/sonar/sonar.md.mustache b/src/main/resources/generator/typescript/sonar/sonar.md.mustache
new file mode 100644
index 000000000..65a9586d6
--- /dev/null
+++ b/src/main/resources/generator/typescript/sonar/sonar.md.mustache
@@ -0,0 +1,29 @@
+# Usage
+
+Start a local Sonar server with:
+
+```bash
+docker compose -f src/main/docker/sonar.yml up -d
+```
+
+You need to wait for Sonar to be up before getting the Sonar token:
+
+```bash
+docker logs -f sonar-token && SONAR_TOKEN=$(docker logs sonar-token)
+```
+
+Install dependencies:
+
+```bash
+npm install
+```
+
+Then, run a Sonar analysis:
+
+```bash
+npx @sonar/scan -Dsonar.token=$SONAR_TOKEN
+```
+
+You can see the result at [http://localhost:9001/](http://localhost:9001/)
+
+[Official documentation](https://www.sonarsource.com/products/sonarqube/)
diff --git a/src/main/resources/generator/typescript/sonar/sonar.yml.mustache b/src/main/resources/generator/typescript/sonar/sonar.yml.mustache
new file mode 100644
index 000000000..e0644c7dc
--- /dev/null
+++ b/src/main/resources/generator/typescript/sonar/sonar.yml.mustache
@@ -0,0 +1,22 @@
+# This configuration is intended for development purpose, it's **your** responsibility to harden it for production
+services:
+  sonar:
+    image: {{sonarqubeDockerImage}}
+    container_name: sonar
+    # Authentication is turned off for out of the box experience while trying out SonarQube
+    # For real use cases delete sonar.forceAuthentication variable or set sonar.forceAuthentication=true
+    environment:
+      - SONAR_FORCEAUTHENTICATION=false
+    # If you want to expose these ports outside your dev PC,
+    # remove the "127.0.0.1:" prefix
+    ports:
+      - '127.0.0.1:9001:9000'
+  sonar-token:
+    build:
+      context: .
+      dockerfile: sonar/Dockerfile
+    container_name: sonar-token
+    environment:
+      - JHIPSTER_SONAR_URL=http://sonar:9000
+    depends_on:
+      - sonar
diff --git a/src/main/resources/generator/typescript/sonar/sonar_generate_token.sh b/src/main/resources/generator/typescript/sonar/sonar_generate_token.sh
new file mode 100755
index 000000000..b7c056c5e
--- /dev/null
+++ b/src/main/resources/generator/typescript/sonar/sonar_generate_token.sh
@@ -0,0 +1,12 @@
+#!/usr/bin/env bash
+
+until [ "$(curl -s $JHIPSTER_SONAR_URL/api/system/status | jq -r .status)" = "UP" ]; do
+  sleep 5
+done
+
+curl -sS -u admin:admin -X POST \
+  "$JHIPSTER_SONAR_URL/api/users/change_password" \
+  -d "login=admin&previousPassword=admin&password=$JHIPSTER_SONAR_PASSWORD"
+
+SONAR_TOKEN=$(curl -sS -u admin:$JHIPSTER_SONAR_PASSWORD -X POST "$JHIPSTER_SONAR_URL/api/user_tokens/generate" -d "name=$JHIPSTER_SONAR_TOKENNAME" | jq -r .token)
+echo $SONAR_TOKEN


Test patch:
diff --git a/src/test/features/sonar-typescript.feature b/src/test/features/sonar-typescript.feature
new file mode 100644
index 000000000..d1cc17285
--- /dev/null
+++ b/src/test/features/sonar-typescript.feature
@@ -0,0 +1,8 @@
+Feature: Sonar-typescript
+
+  Scenario: Should apply sonar typescript module
+    When I apply "sonar-typescript" module to default project with package json without parameters
+    Then I should have files in "."
+      | sonar-project.properties |
+    Then I should have files in "documentation"
+      | sonar.md |
diff --git a/src/test/java/tech/jhipster/lite/generator/typescript/sonar/domain/SonarTypescriptModuleFactoryTest.java b/src/test/java/tech/jhipster/lite/generator/typescript/sonar/domain/SonarTypescriptModuleFactoryTest.java
new file mode 100644
index 000000000..7198d8da3
--- /dev/null
+++ b/src/test/java/tech/jhipster/lite/generator/typescript/sonar/domain/SonarTypescriptModuleFactoryTest.java
@@ -0,0 +1,60 @@
+package tech.jhipster.lite.generator.typescript.sonar.domain;
+
+import static org.mockito.Mockito.when;
+import static tech.jhipster.lite.module.infrastructure.secondary.JHipsterModulesAssertions.*;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import tech.jhipster.lite.TestFileUtils;
+import tech.jhipster.lite.UnitTest;
+import tech.jhipster.lite.module.domain.JHipsterModule;
+import tech.jhipster.lite.module.domain.JHipsterModulesFixture;
+import tech.jhipster.lite.module.domain.docker.DockerImageVersion;
+import tech.jhipster.lite.module.domain.docker.DockerImages;
+import tech.jhipster.lite.module.domain.properties.JHipsterModuleProperties;
+
+@UnitTest
+@ExtendWith(MockitoExtension.class)
+class SonarTypescriptModuleFactoryTest {
+
+  @Mock
+  private DockerImages dockerImages;
+
+  @InjectMocks
+  private SonarTypescriptModuleFactory factory;
+
+  @Test
+  void shouldBuildModule() {
+    mockSonarqubeImage();
+
+    JHipsterModuleProperties properties = JHipsterModulesFixture.propertiesBuilder(TestFileUtils.tmpDirForTest()).build();
+
+    JHipsterModule module = factory.buildModule(properties);
+
+    // @formatter:off
+    assertThatModuleWithFiles(module, packageJsonFile())
+      .hasFile("package.json")
+        .containing(nodeDependency("@sonar/scan"))
+        .and()
+      .hasFile("documentation/sonar.md")
+        .containing("npx @sonar/scan -Dsonar.token=$SONAR_TOKEN")
+        .and()
+      .hasFile("sonar-project.properties")
+        .containing("sonar.javascript.lcov.reportPaths=target/test-results/lcov.info")
+        .and()
+      .hasFile("src/main/docker/sonar.yml")
+        .containing("sonarqube:1.1.1")
+        .and()
+      .hasFile("src/main/docker/sonar/Dockerfile")
+        .and()
+      .hasFile("src/main/docker/sonar/sonar_generate_token.sh");
+    // @formatter:on
+  }
+
+  private void mockSonarqubeImage() {
+    when(dockerImages.get("sonarqube")).thenReturn(new DockerImageVersion("sonarqube", "1.1.1"));
+  }
+}
diff --git a/tests-ci/generate.sh b/tests-ci/generate.sh
index cfbb32f9d..e5a5828b3 100755
--- a/tests-ci/generate.sh
+++ b/tests-ci/generate.sh
@@ -524,7 +524,8 @@ elif [[ $application == 'typescriptapp' ]]; then
     "init" \
     "prettier" \
     "typescript" \
-    "optional-typescript"
+    "optional-typescript" \
+    "sonar-typescript"
 
 elif [[ $application == 'thymeleafapp' ]]; then
   init_server