diff --git a/src/main/java/tech/jhipster/lite/generator/client/react/core/domain/ReactCoreModulesFactory.java b/src/main/java/tech/jhipster/lite/generator/client/react/core/domain/ReactCoreModulesFactory.java index 6971f08f7a9..ce433aaebd6 100644 --- a/src/main/java/tech/jhipster/lite/generator/client/react/core/domain/ReactCoreModulesFactory.java +++ b/src/main/java/tech/jhipster/lite/generator/client/react/core/domain/ReactCoreModulesFactory.java @@ -3,12 +3,14 @@ import static tech.jhipster.lite.module.domain.JHipsterModule.*; import static tech.jhipster.lite.module.domain.npm.JHLiteNpmVersionSource.COMMON; import static tech.jhipster.lite.module.domain.npm.JHLiteNpmVersionSource.REACT; -import static tech.jhipster.lite.module.domain.packagejson.NodeModuleFormat.MODULE; +import java.util.function.Consumer; +import tech.jhipster.lite.module.domain.Indentation; 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.properties.JHipsterModuleProperties; +import tech.jhipster.lite.module.domain.replacement.MandatoryReplacer; public class ReactCoreModulesFactory { @@ -27,36 +29,27 @@ public class ReactCoreModulesFactory { private static final JHipsterDestination PRIMARY_APP_DESTINATION = APP_DESTINATION.append(PRIMARY_APP); private static final String TEST_PRIMARY = "src/test/webapp/unit/common/primary/app"; + private static final String DEFAULT_TSCONFIG_PATH = "\"@/*\": [\"src/main/webapp/app/*\"]"; public JHipsterModule buildModule(JHipsterModuleProperties properties) { //@formatter:off return moduleBuilder(properties) .preCommitActions(stagedFilesFilter("{src/**/,}*.{ts,tsx}"), preCommitCommands("eslint --fix", "prettier --write")) .packageJson() - .type(MODULE) + .removeDevDependency(packageName("@tsconfig/recommended")) .addDevDependency(packageName("@testing-library/dom"), REACT) .addDevDependency(packageName("@testing-library/react"), REACT) .addDevDependency(packageName("@types/node"), COMMON) .addDevDependency(packageName("@types/react"), REACT) .addDevDependency(packageName("@types/react-dom"), REACT) .addDevDependency(packageName("@tsconfig/vite-react"), REACT) - .addDevDependency(packageName("@typescript-eslint/eslint-plugin"), COMMON) .addDevDependency(packageName("@vitejs/plugin-react"), REACT) - .addDevDependency(packageName("@vitest/coverage-istanbul"), COMMON) - .addDevDependency(packageName("typescript-eslint"), COMMON) - .addDevDependency(packageName("globals"), COMMON) - .addDevDependency(packageName("eslint"), COMMON) .addDevDependency(packageName("eslint-plugin-react"), REACT) .addDevDependency(packageName("jsdom"), COMMON) - .addDevDependency(packageName("typescript"), COMMON) .addDevDependency(packageName("ts-node"), REACT) .addDevDependency(packageName("vite"), COMMON) - .addDevDependency(packageName("vite-tsconfig-paths"), COMMON) - .addDevDependency(packageName("vitest"), COMMON) - .addDevDependency(packageName("vitest-sonar-reporter"), COMMON) .addDependency(packageName("react"), REACT) .addDependency(packageName("react-dom"), REACT) - .addDevDependency(packageName("npm-run-all2"), COMMON) .addScript(scriptKey("dev"), scriptCommand("npm-run-all dev:*")) .addScript(scriptKey("dev:vite"), scriptCommand("vite")) .addScript(scriptKey("build"), scriptCommand("npm-run-all build:*")) @@ -64,19 +57,10 @@ public JHipsterModule buildModule(JHipsterModuleProperties properties) { .addScript(scriptKey("build:vite"), scriptCommand("vite build --emptyOutDir")) .addScript(scriptKey("preview"), scriptCommand("vite preview")) .addScript(scriptKey("start"), scriptCommand("vite")) - .addScript(scriptKey("lint"), scriptCommand("eslint .")) - .addScript(scriptKey("watch"), scriptCommand("npm-run-all --parallel watch:*")) - .addScript(scriptKey("watch:tsc"), scriptCommand("tsc --noEmit --watch")) - .addScript(scriptKey("test"), scriptCommand("npm run watch:test")) - .addScript(scriptKey("test:coverage"), scriptCommand("vitest run --coverage")) - .addScript(scriptKey("watch:test"), scriptCommand("vitest --")) .and() .files() .batch(SOURCE, to(".")) - .addFile("tsconfig.json") .addTemplate("vite.config.ts") - .addTemplate("vitest.config.ts") - .addTemplate("eslint.config.js") .and() .batch(APP_SOURCE, APP_DESTINATION) .addTemplate("index.css") @@ -92,7 +76,93 @@ public JHipsterModule buildModule(JHipsterModuleProperties properties) { .addFile("ReactLogo.png") .and() .and() + .apply(patchEslintConfig(properties)) + .apply(patchTsConfig(properties)) + .apply(patchVitestConfig(properties)) .build(); //@formatter:on } + + private Consumer patchEslintConfig(JHipsterModuleProperties properties) { + String reactConfig = + """ + \t\tfiles: ['src/main/webapp/**/*.{ts,tsx}', 'src/main/webapp/**/*.spec.ts'], + \t\textends: [...typescript.configs.recommendedTypeChecked, react], + \t\tsettings: { + \t\t\treact: { + \t\t\t\tversion: 'detect', + \t\t\t}, + \t\t},\ + """.replace("\t", properties.indentation().spaces()); + //@formatter:off + return moduleBuilder -> moduleBuilder + .mandatoryReplacements() + .in(path("eslint.config.js")) + .add(lineAfterRegex("from 'typescript-eslint'"), "import react from 'eslint-plugin-react/configs/recommended.js';") + .add(regex("\\s+\\.\\.\\.typescript\\.configs\\.recommended.*"), "") + .add(regex("[ \\t]+files: \\['src/\\*/webapp/\\*\\*\\/\\*\\.ts'],"), reactConfig) + .add( + lineAfterRegex("globals: \\{ \\.\\.\\.globals\\.browser },"), + """ + \t\t\tparserOptions: { + \t\t\t\tproject: ['./tsconfig.json'], + \t\t\t},\ + """.replace("\t", properties.indentation().spaces()) + ) + .add( + regex("[ \\t]+quotes: \\['error', 'single', \\{ avoidEscape: true }],"), + """ + \t\t\t'react/react-in-jsx-scope': 'off', + \t\t\t'@typescript-eslint/no-misused-promises': [ + \t\t\t\t'error', + \t\t\t\t{ + \t\t\t\t\tchecksVoidReturn: false, + \t\t\t\t} + \t\t\t],\ + """.replace("\t", properties.indentation().spaces()) + ) + .and() + .and(); + //@formatter:on + } + + private Consumer patchTsConfig(JHipsterModuleProperties properties) { + String pathsReplacement = + DEFAULT_TSCONFIG_PATH + "," + LINE_BREAK + properties.indentation().times(3) + "\"@assets/*\": [\"src/main/webapp/assets/*\"]"; + //@formatter:off + return moduleBuilder -> moduleBuilder + .mandatoryReplacements() + .in(path("tsconfig.json")) + .add(text("@tsconfig/recommended/tsconfig.json"), "@tsconfig/vite-react/tsconfig.json") + .add(tsConfigCompilerOption("composite", false, properties.indentation())) + .add(tsConfigCompilerOption("forceConsistentCasingInFileNames", true, properties.indentation())) + .add(tsConfigCompilerOption("allowSyntheticDefaultImports", true, properties.indentation())) + .add(text(DEFAULT_TSCONFIG_PATH), pathsReplacement) + .and() + .and(); + //@formatter:on + } + + private static MandatoryReplacer tsConfigCompilerOption(String optionName, boolean optionValue, Indentation indentation) { + String compilerOption = indentation.times(2) + "\"%s\": %s,".formatted(optionName, optionValue); + return new MandatoryReplacer(lineAfterRegex("\"compilerOptions\":"), compilerOption); + } + + private Consumer patchVitestConfig(JHipsterModuleProperties properties) { + //@formatter:off + return moduleBuilder -> moduleBuilder + .mandatoryReplacements() + .in(path("vitest.config.ts")) + .add(lineAfterRegex("from 'vitest/config';"), "import react from '@vitejs/plugin-react';") + .add(text( "plugins: ["), "plugins: [react(), ") + .add(text("environment: 'node',"), "environment: 'jsdom',") + .add(vitestCoverageExclusion(properties,"src/main/webapp/app/index.tsx")) + .and(); + //@formatter:on + } + + private static MandatoryReplacer vitestCoverageExclusion(JHipsterModuleProperties properties, String filePattern) { + Indentation indentation = properties.indentation(); + return new MandatoryReplacer(lineAfterRegex("configDefaults.coverage.exclude"), indentation.times(4) + "'" + filePattern + "',"); + } } diff --git a/src/main/java/tech/jhipster/lite/generator/client/react/core/infrastructure/primary/ReactCoreModulesConfiguration.java b/src/main/java/tech/jhipster/lite/generator/client/react/core/infrastructure/primary/ReactCoreModulesConfiguration.java index e9c49651f66..a62c0e7fd12 100644 --- a/src/main/java/tech/jhipster/lite/generator/client/react/core/infrastructure/primary/ReactCoreModulesConfiguration.java +++ b/src/main/java/tech/jhipster/lite/generator/client/react/core/infrastructure/primary/ReactCoreModulesConfiguration.java @@ -1,14 +1,12 @@ package tech.jhipster.lite.generator.client.react.core.infrastructure.primary; -import static tech.jhipster.lite.generator.slug.domain.JHLiteFeatureSlug.*; +import static tech.jhipster.lite.generator.slug.domain.JHLiteFeatureSlug.CLIENT_CORE; 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.client.react.core.application.ReactCoreApplicationService; -import tech.jhipster.lite.module.domain.resource.JHipsterModuleOrganization; -import tech.jhipster.lite.module.domain.resource.JHipsterModulePropertiesDefinition; -import tech.jhipster.lite.module.domain.resource.JHipsterModuleResource; +import tech.jhipster.lite.module.domain.resource.*; @Configuration class ReactCoreModulesConfiguration { @@ -21,7 +19,7 @@ JHipsterModuleResource reactCoreModule(ReactCoreApplicationService react) { .slug(REACT_CORE) .propertiesDefinition(properties()) .apiDoc("Frontend - React", "Add React+Vite with minimal CSS") - .organization(JHipsterModuleOrganization.builder().feature(CLIENT_CORE).addDependency(INIT).addDependency(PRETTIER).build()) + .organization(JHipsterModuleOrganization.builder().feature(CLIENT_CORE).addDependency(TYPESCRIPT).addDependency(PRETTIER).build()) .tags("client", REACT) .factory(react::buildModule); } diff --git a/src/test/features/client/react-i18n.feature b/src/test/features/client/react-i18n.feature index 38eaf1d4b93..d97724c1748 100644 --- a/src/test/features/client/react-i18n.feature +++ b/src/test/features/client/react-i18n.feature @@ -3,6 +3,7 @@ Feature: React i18n Scenario: Should apply react i18n module to react When I apply modules to default project | init | + | typescript | | react-core | | react-i18next | Then I should have files in "src/main/webapp/app" diff --git a/src/test/features/client/react-jwt.feature b/src/test/features/client/react-jwt.feature index 898a0eef358..566a15b6fca 100644 --- a/src/test/features/client/react-jwt.feature +++ b/src/test/features/client/react-jwt.feature @@ -3,6 +3,7 @@ Feature: React JWT Scenario: Should apply react jwt module to react When I apply modules to default project | init | + | typescript | | react-core | | react-jwt | Then I should have files in "src/main/webapp/app/common/services" diff --git a/src/test/features/client/react.feature b/src/test/features/client/react.feature index ebb798ee937..51e6071f315 100644 --- a/src/test/features/client/react.feature +++ b/src/test/features/client/react.feature @@ -4,6 +4,7 @@ Feature: React modules When I apply modules to default project | init | | prettier | + | typescript | | react-core | Then I should have files in "src/main/webapp/app/common/primary/app" | App.tsx | diff --git a/src/test/java/tech/jhipster/lite/generator/client/react/core/domain/ReactCoreModulesFactoryTest.java b/src/test/java/tech/jhipster/lite/generator/client/react/core/domain/ReactCoreModulesFactoryTest.java index 46c5f031680..778052bab4e 100644 --- a/src/test/java/tech/jhipster/lite/generator/client/react/core/domain/ReactCoreModulesFactoryTest.java +++ b/src/test/java/tech/jhipster/lite/generator/client/react/core/domain/ReactCoreModulesFactoryTest.java @@ -19,45 +19,29 @@ void shouldBuildModuleWithStyle() { JHipsterModulesFixture.propertiesBuilder(TestFileUtils.tmpDirForTest()).projectBaseName("jhipster").build() ); - assertThatModuleWithFiles(module, packageJsonFile(), lintStagedConfigFile()) + assertThatModuleWithFiles(module, packageJsonFile(), lintStagedConfigFile(), eslintConfigFile(), tsConfigFile(), vitestConfigFile()) .hasFile("package.json") - .containing("\"type\": \"module\"") + .notContaining(nodeDependency("@tsconfig/recommended")) .containing(nodeDependency("@testing-library/dom")) .containing(nodeDependency("@testing-library/react")) .containing(nodeDependency("@types/node")) .containing(nodeDependency("@types/react")) .containing(nodeDependency("@types/react-dom")) .containing(nodeDependency("@tsconfig/vite-react")) - .containing(nodeDependency("@typescript-eslint/eslint-plugin")) .containing(nodeDependency("@vitejs/plugin-react")) - .containing(nodeDependency("@vitest/coverage-istanbul")) - .containing(nodeDependency("typescript-eslint")) - .containing(nodeDependency("globals")) - .containing(nodeDependency("eslint")) .containing(nodeDependency("eslint-plugin-react")) .containing(nodeDependency("jsdom")) - .containing(nodeDependency("typescript")) .containing(nodeDependency("ts-node")) .containing(nodeDependency("vite")) - .containing(nodeDependency("vite-tsconfig-paths")) - .containing(nodeDependency("vitest")) - .containing(nodeDependency("vitest-sonar-reporter")) .containing(nodeDependency("react")) - .containing(nodeDependency("npm-run-all2")) .containing(nodeDependency("react-dom")) .containing(nodeScript("dev", "npm-run-all dev:*")) .containing(nodeScript("dev:vite", "vite")) .containing(nodeScript("build", "npm-run-all build:*")) .containing(nodeScript("build:tsc", "tsc")) .containing(nodeScript("build:vite", "vite build --emptyOutDir")) - .containing(nodeScript("watch", "npm-run-all --parallel watch:*")) - .containing(nodeScript("watch:tsc", "tsc --noEmit --watch")) .containing(nodeScript("preview", "vite preview")) .containing(nodeScript("start", "vite")) - .containing(nodeScript("lint", "eslint .")) - .containing(nodeScript("test", "npm run watch:test")) - .containing(nodeScript("test:coverage", "vitest run --coverage")) - .containing(nodeScript("watch:test", "vitest --")) .and() .hasFile(".lintstagedrc.cjs") .containing( @@ -69,7 +53,16 @@ void shouldBuildModuleWithStyle() { """ ) .and() - .hasFiles("tsconfig.json", "vite.config.ts", "vitest.config.ts", "eslint.config.js") + .hasFile("eslint.config.js") + .matchingSavedSnapshot() + .and() + .hasFile("tsconfig.json") + .matchingSavedSnapshot() + .and() + .hasFile("vitest.config.ts") + .matchingSavedSnapshot() + .and() + .hasFiles("vite.config.ts") .hasFiles("src/main/webapp/index.html") .hasPrefixedFiles("src/main/webapp/app", "index.css", "index.tsx", "vite-env.d.ts") .hasFiles("src/test/webapp/unit/common/primary/app/App.spec.tsx") @@ -86,7 +79,7 @@ void shouldViteConfigBeUpdatedWhenServerPortPropertyNotDefault() { JHipsterModulesFixture.propertiesBuilder(TestFileUtils.tmpDirForTest()).projectBaseName("jhipster").put("serverPort", 8081).build() ); - assertThatModuleWithFiles(module, packageJsonFile()) + assertThatModuleWithFiles(module, packageJsonFile(), eslintConfigFile(), tsConfigFile(), vitestConfigFile()) .hasFile("vite.config.ts") .containing("localhost:8081") .notContaining("localhost:8080"); @@ -98,7 +91,9 @@ void shouldViteConfigBeDefaultWhenServerPortPropertyMissing() { JHipsterModulesFixture.propertiesBuilder(TestFileUtils.tmpDirForTest()).projectBaseName("jhipster").build() ); - assertThatModuleWithFiles(module, packageJsonFile()).hasFile("vite.config.ts").containing("localhost:8080"); + assertThatModuleWithFiles(module, packageJsonFile(), eslintConfigFile(), tsConfigFile(), vitestConfigFile()) + .hasFile("vite.config.ts") + .containing("localhost:8080"); } private String nodeScript(String key, String value) { diff --git a/src/main/resources/generator/client/react/core/eslint.config.js.mustache b/src/test/resources/tech/jhipster/lite/generator/client/react/core/domain/ReactCoreModulesFactoryTest.shouldBuildModuleWithStyle.eslint.config.js.approved.txt similarity index 79% rename from src/main/resources/generator/client/react/core/eslint.config.js.mustache rename to src/test/resources/tech/jhipster/lite/generator/client/react/core/domain/ReactCoreModulesFactoryTest.shouldBuildModuleWithStyle.eslint.config.js.approved.txt index 5e0322fd429..e8134c13e44 100644 --- a/src/main/resources/generator/client/react/core/eslint.config.js.mustache +++ b/src/test/resources/tech/jhipster/lite/generator/client/react/core/domain/ReactCoreModulesFactoryTest.shouldBuildModuleWithStyle.eslint.config.js.approved.txt @@ -1,6 +1,6 @@ +import js from '@eslint/js'; import globals from 'globals'; import typescript from 'typescript-eslint'; -import eslint from '@eslint/js'; import react from 'eslint-plugin-react/configs/recommended.js'; export default typescript.config( @@ -12,9 +12,9 @@ export default typescript.config( }, }, { - ignores: ['{{projectBuildDirectory}}/'], + ignores: ['target/'], }, - eslint.configs.recommended, + js.configs.recommended, { files: ['src/main/webapp/**/*.{ts,tsx}', 'src/main/webapp/**/*.spec.ts'], extends: [...typescript.configs.recommendedTypeChecked, react], @@ -24,9 +24,7 @@ export default typescript.config( }, }, languageOptions: { - globals: { - ...globals.browser, - }, + globals: { ...globals.browser }, parserOptions: { project: ['./tsconfig.json'], }, @@ -36,7 +34,7 @@ export default typescript.config( '@typescript-eslint/no-misused-promises': [ 'error', { - 'checksVoidReturn': false + checksVoidReturn: false, } ], }, diff --git a/src/main/resources/generator/client/react/core/tsconfig.json b/src/test/resources/tech/jhipster/lite/generator/client/react/core/domain/ReactCoreModulesFactoryTest.shouldBuildModuleWithStyle.tsconfig.json.approved.txt similarity index 100% rename from src/main/resources/generator/client/react/core/tsconfig.json rename to src/test/resources/tech/jhipster/lite/generator/client/react/core/domain/ReactCoreModulesFactoryTest.shouldBuildModuleWithStyle.tsconfig.json.approved.txt index bbaf02392b0..480eede51b5 100644 --- a/src/main/resources/generator/client/react/core/tsconfig.json +++ b/src/test/resources/tech/jhipster/lite/generator/client/react/core/domain/ReactCoreModulesFactoryTest.shouldBuildModuleWithStyle.tsconfig.json.approved.txt @@ -4,8 +4,8 @@ "allowSyntheticDefaultImports": true, "forceConsistentCasingInFileNames": true, "composite": false, - "baseUrl": ".", "types": ["vitest/globals"], + "baseUrl": ".", "paths": { "@/*": ["src/main/webapp/app/*"], "@assets/*": ["src/main/webapp/assets/*"] diff --git a/src/main/resources/generator/client/react/core/vitest.config.ts.mustache b/src/test/resources/tech/jhipster/lite/generator/client/react/core/domain/ReactCoreModulesFactoryTest.shouldBuildModuleWithStyle.vitest.config.ts.approved.txt similarity index 87% rename from src/main/resources/generator/client/react/core/vitest.config.ts.mustache rename to src/test/resources/tech/jhipster/lite/generator/client/react/core/domain/ReactCoreModulesFactoryTest.shouldBuildModuleWithStyle.vitest.config.ts.approved.txt index 8fc2f51dd20..5bdbdda98a2 100644 --- a/src/main/resources/generator/client/react/core/vitest.config.ts.mustache +++ b/src/test/resources/tech/jhipster/lite/generator/client/react/core/domain/ReactCoreModulesFactoryTest.shouldBuildModuleWithStyle.vitest.config.ts.approved.txt @@ -9,7 +9,7 @@ export default defineConfig({ test: { reporters: ['verbose', 'vitest-sonar-reporter'], outputFile: { - 'vitest-sonar-reporter': '{{projectBuildDirectory}}/test-results/TESTS-results-sonar.xml', + 'vitest-sonar-reporter': 'target/test-results/TESTS-results-sonar.xml', }, globals: true, logHeapUsage: true, @@ -34,7 +34,7 @@ export default defineConfig({ 'src/main/webapp/app/index.tsx', ], provider: 'istanbul', - reportsDirectory: '{{projectBuildDirectory}}/test-results/', + reportsDirectory: 'target/test-results/', reporter: ['html', 'json-summary', 'text', 'text-summary', 'lcov', 'clover'], watermarks: { statements: [100, 100], diff --git a/tests-ci/generate.sh b/tests-ci/generate.sh index b35c790026c..cc8a31acdb9 100755 --- a/tests-ci/generate.sh +++ b/tests-ci/generate.sh @@ -402,6 +402,7 @@ elif [[ $application == 'reactapp' ]]; then frontend_server_plugin applyModules \ "prettier" \ + "typescript" \ "react-core" \ "cypress-component-tests"