Repository: jhipster/jhipster-lite
Pull request #10404 (2024-07-27 07:03:11)

TS Loader module
Small module to add a Loader object for frontend applications


Patch:
diff --git a/src/main/java/tech/jhipster/lite/generator/client/loader/application/TsLoaderModuleApplicationService.java b/src/main/java/tech/jhipster/lite/generator/client/loader/application/TsLoaderModuleApplicationService.java
new file mode 100644
index 000000000..5af52b48a
--- /dev/null
+++ b/src/main/java/tech/jhipster/lite/generator/client/loader/application/TsLoaderModuleApplicationService.java
@@ -0,0 +1,20 @@
+package tech.jhipster.lite.generator.client.loader.application;
+
+import org.springframework.stereotype.Service;
+import tech.jhipster.lite.generator.client.loader.domain.TsLoaderModuleFactory;
+import tech.jhipster.lite.module.domain.JHipsterModule;
+import tech.jhipster.lite.module.domain.properties.JHipsterModuleProperties;
+
+@Service
+public class TsLoaderModuleApplicationService {
+
+  private final TsLoaderModuleFactory factory;
+
+  public TsLoaderModuleApplicationService() {
+    this.factory = new TsLoaderModuleFactory();
+  }
+
+  public JHipsterModule buildModule(JHipsterModuleProperties properties) {
+    return factory.buildModule(properties);
+  }
+}
diff --git a/src/main/java/tech/jhipster/lite/generator/client/loader/domain/TsLoaderModuleFactory.java b/src/main/java/tech/jhipster/lite/generator/client/loader/domain/TsLoaderModuleFactory.java
new file mode 100644
index 000000000..72f7c075d
--- /dev/null
+++ b/src/main/java/tech/jhipster/lite/generator/client/loader/domain/TsLoaderModuleFactory.java
@@ -0,0 +1,25 @@
+package tech.jhipster.lite.generator.client.loader.domain;
+
+import static tech.jhipster.lite.module.domain.JHipsterModule.*;
+
+import tech.jhipster.lite.module.domain.JHipsterModule;
+import tech.jhipster.lite.module.domain.file.JHipsterSource;
+import tech.jhipster.lite.module.domain.properties.JHipsterModuleProperties;
+
+public class TsLoaderModuleFactory {
+
+  public JHipsterModule buildModule(JHipsterModuleProperties properties) {
+    JHipsterSource source = from("client/loader");
+
+    String destination = "shared/loader/infrastructure/primary";
+
+    //@formatter:off
+    return moduleBuilder(properties)
+      .files()
+        .add(source.template("Loader.ts"), to("src/main/webapp/app").append(destination).append("Loader.ts"))
+        .add(source.template("Loader.spec.ts"), to("src/test/javascript/spec").append(destination).append("Loader.spec.ts"))
+        .and()
+      .build();
+    //@formatter:on
+  }
+}
diff --git a/src/main/java/tech/jhipster/lite/generator/client/loader/infrastructure/primary/TsLoaderModuleConfiguration.java b/src/main/java/tech/jhipster/lite/generator/client/loader/infrastructure/primary/TsLoaderModuleConfiguration.java
new file mode 100644
index 000000000..9a0c2cc6f
--- /dev/null
+++ b/src/main/java/tech/jhipster/lite/generator/client/loader/infrastructure/primary/TsLoaderModuleConfiguration.java
@@ -0,0 +1,23 @@
+package tech.jhipster.lite.generator.client.loader.infrastructure.primary;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import tech.jhipster.lite.generator.client.loader.application.TsLoaderModuleApplicationService;
+import tech.jhipster.lite.generator.slug.domain.JHLiteModuleSlug;
+import tech.jhipster.lite.module.domain.resource.JHipsterModuleOrganization;
+import tech.jhipster.lite.module.domain.resource.JHipsterModuleResource;
+
+@Configuration
+class TsLoaderModuleConfiguration {
+
+  @Bean
+  JHipsterModuleResource tsLoaderModule(TsLoaderModuleApplicationService tsLoader) {
+    return JHipsterModuleResource.builder()
+      .slug(JHLiteModuleSlug.TS_LOADER)
+      .withoutProperties()
+      .apiDoc("Client", "Helper class to represent loading states")
+      .organization(JHipsterModuleOrganization.builder().addDependency(JHLiteModuleSlug.INIT).build())
+      .tags("client")
+      .factory(tsLoader::buildModule);
+  }
+}
diff --git a/src/main/java/tech/jhipster/lite/generator/client/loader/package-info.java b/src/main/java/tech/jhipster/lite/generator/client/loader/package-info.java
new file mode 100644
index 000000000..2fb5b26bc
--- /dev/null
+++ b/src/main/java/tech/jhipster/lite/generator/client/loader/package-info.java
@@ -0,0 +1,2 @@
+@tech.jhipster.lite.SharedKernel
+package tech.jhipster.lite.generator.client.loader;
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 65469a17d..66df7cb99 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
@@ -136,6 +136,7 @@ public enum JHLiteModuleSlug implements JHipsterModuleSlugFactory {
   SPRINGDOC_OAUTH_2_AUTH_0("springdoc-oauth2-auth0"),
   SPRINGDOC_OAUTH_2_OKTA("springdoc-oauth2-okta"),
   SPRINGDOC_WEBFLUX_OPENAPI("springdoc-webflux-openapi"),
+  TS_LOADER("ts-loader"),
   WEBJARS_LOCATOR("webjars-locator"),
   HTMX_WEBJARS("htmx-webjars"),
   ALPINE_JS_WEBJARS("alpinejs-webjars"),
diff --git a/src/main/resources/generator/client/loader/Loader.spec.ts.mustache b/src/main/resources/generator/client/loader/Loader.spec.ts.mustache
new file mode 100644
index 000000000..1195284e1
--- /dev/null
+++ b/src/main/resources/generator/client/loader/Loader.spec.ts.mustache
@@ -0,0 +1,55 @@
+import { describe, expect, it } from 'vitest';
+import { Loader, LoadingState } from '@/shared/loader/infrastructure/primary/Loader.ts';
+
+describe('Loader', () => {
+  describe('Loading loader', () => {
+    const loader = Loader.loading<string>();
+
+    it('should be in a loading state', () => {
+      expect(loader.state()).toBe(LoadingState.LOADING);
+      expect(loader.isLoading()).toBe(true);
+      expect(loader.isLoaded()).toBe(false);
+      expect(loader.isInError()).toBe(false);
+    });
+
+    it('should not get value', () => {
+      expect(() => loader.value()).toThrowError();
+    });
+
+    it('should not get error', () => {
+      expect(() => loader.error()).toThrowError();
+    });
+  });
+
+  describe('Loaded loader', () => {
+    const loader = Loader.loaded('value');
+
+    it('should be loaded', () => {
+      expect(loader.state()).toBe(LoadingState.LOADED);
+      expect(loader.isLoading()).toBe(false);
+      expect(loader.isLoaded()).toBe(true);
+      expect(loader.isInError()).toBe(false);
+      expect(loader.value()).toBe('value');
+    });
+
+    it('should not get error', () => {
+      expect(() => loader.error()).toThrowError();
+    });
+  });
+
+  describe('Error loader', () => {
+    const loader = Loader.error(new Error('oops'));
+
+    it('should be in error state', () => {
+      expect(loader.state()).toBe(LoadingState.ERROR);
+      expect(loader.isLoading()).toBe(false);
+      expect(loader.isLoaded()).toBe(false);
+      expect(loader.isInError()).toBe(true);
+      expect(loader.error().message).toBe('oops');
+    });
+
+    it('should not get value', () => {
+      expect(() => loader.value()).toThrowError();
+    });
+  });
+});
diff --git a/src/main/resources/generator/client/loader/Loader.ts.mustache b/src/main/resources/generator/client/loader/Loader.ts.mustache
new file mode 100644
index 000000000..aada05a63
--- /dev/null
+++ b/src/main/resources/generator/client/loader/Loader.ts.mustache
@@ -0,0 +1,87 @@
+export abstract class Loader<T> {
+  static loading<T>(): Loader<T> {
+    return new LoadingLoader();
+  }
+
+  static loaded<T>(value: T): Loader<T> {
+    return new LoadedLoader(value);
+  }
+
+  static error<T>(error: Error): Loader<T> {
+    return new ErrorLoader(error);
+  }
+
+  isLoading(): boolean {
+    return this.state() === LoadingState.LOADING;
+  }
+
+  isLoaded(): boolean {
+    return this.state() === LoadingState.LOADED;
+  }
+
+  isInError(): boolean {
+    return this.state() === LoadingState.ERROR;
+  }
+
+  abstract state(): LoadingState;
+
+  abstract value(): T;
+
+  abstract error(): Error;
+}
+
+export enum LoadingState {
+  LOADING,
+  LOADED,
+  ERROR,
+}
+
+class LoadingLoader<T> extends Loader<T> {
+  state(): LoadingState {
+    return LoadingState.LOADING;
+  }
+
+  value(): T {
+    throw new Error("Can't get value for a loading loaded");
+  }
+
+  error(): Error {
+    throw new Error("Can't get error for loading loader");
+  }
+}
+
+class LoadedLoader<T> extends Loader<T> {
+  constructor(private readonly _value: T) {
+    super();
+  }
+
+  state(): LoadingState {
+    return LoadingState.LOADED;
+  }
+
+  value(): T {
+    return this._value;
+  }
+
+  error(): Error {
+    throw new Error("Can't get error for loaded loader");
+  }
+}
+
+class ErrorLoader<T> extends Loader<T> {
+  constructor(private readonly _error: Error) {
+    super();
+  }
+
+  state(): LoadingState {
+    return LoadingState.ERROR;
+  }
+
+  value(): T {
+    throw new Error("Can't get value for error loader");
+  }
+
+  error(): Error {
+    return this._error;
+  }
+}


Test patch:
diff --git a/src/test/features/client/loader.feature b/src/test/features/client/loader.feature
new file mode 100644
index 000000000..54157f6c8
--- /dev/null
+++ b/src/test/features/client/loader.feature
@@ -0,0 +1,9 @@
+Feature: TS Loader
+
+  Scenario: Should apply TS Loader module
+    When I apply modules to default project
+      | init      |
+      | prettier  |
+      | ts-loader |
+    Then I should have files in "src/main/webapp/app"
+      | shared/loader/infrastructure/primary/Loader.ts |
diff --git a/src/test/java/tech/jhipster/lite/generator/client/loader/domain/TsLoaderModuleFactoryTest.java b/src/test/java/tech/jhipster/lite/generator/client/loader/domain/TsLoaderModuleFactoryTest.java
new file mode 100644
index 000000000..ca0307eba
--- /dev/null
+++ b/src/test/java/tech/jhipster/lite/generator/client/loader/domain/TsLoaderModuleFactoryTest.java
@@ -0,0 +1,28 @@
+package tech.jhipster.lite.generator.client.loader.domain;
+
+import static tech.jhipster.lite.module.infrastructure.secondary.JHipsterModulesAssertions.*;
+
+import org.junit.jupiter.api.Test;
+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.properties.JHipsterModuleProperties;
+
+@UnitTest
+class TsLoaderModuleFactoryTest {
+
+  private static TsLoaderModuleFactory factory = new TsLoaderModuleFactory();
+
+  @Test
+  void shouldBuildModule() {
+    JHipsterModuleProperties properties = JHipsterModulesFixture.propertiesBuilder(TestFileUtils.tmpDirForTest()).build();
+
+    JHipsterModule module = factory.buildModule(properties);
+
+    assertThatModuleWithFiles(module, packageJsonFile())
+      .hasFile("src/main/webapp/app/shared/loader/infrastructure/primary/Loader.ts")
+      .and()
+      .hasFile("src/test/javascript/spec/shared/loader/infrastructure/primary/Loader.spec.ts");
+  }
+}
diff --git a/tests-ci/generate.sh b/tests-ci/generate.sh
index 6c1f3973d..734837596 100755
--- a/tests-ci/generate.sh
+++ b/tests-ci/generate.sh
@@ -134,6 +134,7 @@ elif [[ $application == 'fullapp' ]]; then
 
   applyModules \
   "prettier" \
+  "ts-loader" \
   "infinitest-filters" \
   "pagination-domain" \
   "rest-pagination" \