diff --git a/generators/app/__snapshots__/generator.spec.ts.snap b/generators/app/__snapshots__/generator.spec.ts.snap index fb493a563456..f571c8a49450 100644 --- a/generators/app/__snapshots__/generator.spec.ts.snap +++ b/generators/app/__snapshots__/generator.spec.ts.snap @@ -240,6 +240,7 @@ exports[`generator - app with default config should match snapshot 1`] = ` "blueprints": [], "buildTool": "maven", "buildToolAny": true, + "buildToolExecutable": "mvnw", "buildToolGradle": false, "buildToolMaven": true, "buildToolUnknown": false, @@ -909,6 +910,7 @@ exports[`generator - app with gateway should match snapshot 1`] = ` "blueprints": [], "buildTool": "maven", "buildToolAny": true, + "buildToolExecutable": "mvnw", "buildToolGradle": false, "buildToolMaven": true, "buildToolUnknown": false, @@ -1572,6 +1574,7 @@ exports[`generator - app with microservice should match snapshot 1`] = ` "blueprints": [], "buildTool": "maven", "buildToolAny": true, + "buildToolExecutable": "mvnw", "buildToolGradle": false, "buildToolMaven": true, "buildToolUnknown": false, diff --git a/generators/base-application/types.d.ts b/generators/base-application/types.d.ts index c1b3279e0389..597293f0c68b 100644 --- a/generators/base-application/types.d.ts +++ b/generators/base-application/types.d.ts @@ -148,6 +148,8 @@ export type CommonClientServerApplication = BaseApplication & SpringBootApplication & ClientApplication & ExportApplicationPropertiesFromCommand & + ExportApplicationPropertiesFromCommand & + import('../docker/types.d.ts').DockerApplicationType & ExportApplicationPropertiesFromCommand & ApplicationProperties & { clientRootDir: string; diff --git a/generators/base/shared-data.ts b/generators/base/shared-data.ts index ac95dd54066a..6e2a4c6860d9 100644 --- a/generators/base/shared-data.ts +++ b/generators/base/shared-data.ts @@ -20,6 +20,7 @@ import assert from 'node:assert'; import { existsSync, readFileSync, statSync } from 'fs'; import { rm } from 'fs/promises'; import { isAbsolute, join, relative } from 'path'; +import { execaCommandSync } from 'execa'; import { lt as semverLessThan } from 'semver'; import { defaults } from 'lodash-es'; import type { MemFsEditor } from 'mem-fs-editor'; @@ -136,6 +137,18 @@ export default class SharedData Entity[]; filterEntitiesAndPropertiesForClient?: (entity: Entity[]) => Entity[]; filterEntityPropertiesForClient?: (entity: Entity) => Entity; diff --git a/generators/docker-compose/generator.ts b/generators/docker-compose/generator.ts index 6276bae5672e..4915ba699853 100644 --- a/generators/docker-compose/generator.ts +++ b/generators/docker-compose/generator.ts @@ -65,11 +65,10 @@ export default class DockerComposeGenerator extends BaseWorkspacesGenerator { this.log.log(chalk.white(`Files will be generated in folder: ${chalk.yellow(this.destinationRoot())}`)); }, checkDocker, - async checkDockerCompose() { + async checkDockerCompose({ control }) { if (this.skipChecks) return; - const { exitCode } = await this.spawnCommand('docker compose version', { reject: false, stdio: 'pipe' }); - if (exitCode !== 0) { + if (!control.enviromentHasDockerCompose) { throw new Error(`Docker Compose V2 is not installed on your computer. Read https://docs.docker.com/compose/install/ `); diff --git a/generators/docker/command.ts b/generators/docker/command.ts new file mode 100644 index 000000000000..53824bf4168d --- /dev/null +++ b/generators/docker/command.ts @@ -0,0 +1,26 @@ +/** + * Copyright 2013-2024 the original author or authors from the JHipster project. + * + * This file is part of the JHipster project, see https://www.jhipster.tech/ + * for more information. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import type { JHipsterCommandDefinition } from '../../lib/command/index.js'; + +const command = { + configs: {}, + import: [], +} as const satisfies JHipsterCommandDefinition; + +export default command; diff --git a/generators/docker/generator.ts b/generators/docker/generator.ts index 66f9b1011846..27cd74030646 100644 --- a/generators/docker/generator.ts +++ b/generators/docker/generator.ts @@ -1,4 +1,3 @@ -// @ts-nocheck /** * Copyright 2013-2024 the original author or authors from the JHipster project. * @@ -57,29 +56,30 @@ export default class DockerGenerator extends BaseApplicationGenerator { get preparing() { return this.asPreparingTaskGroup({ dockerServices({ application }) { + const dockerServices = application.dockerServices!; if (application.backendTypeSpringBoot) { - application.dockerServices.push('app'); + dockerServices.push('app'); } if (application.authenticationTypeOauth2) { - application.dockerServices.push('keycloak'); + dockerServices.push('keycloak'); } if (application.searchEngineElasticsearch) { - application.dockerServices.push('elasticsearch'); + dockerServices.push('elasticsearch'); } if (application.messageBrokerKafka || application.messageBrokerPulsar) { - application.dockerServices.push(application.messageBroker); + dockerServices.push(application.messageBroker!); } if (application.serviceDiscoveryConsul || application.serviceDiscoveryEureka) { - application.dockerServices.push(application.serviceDiscoveryType); + dockerServices.push(application.serviceDiscoveryType!); } if (application.serviceDiscoveryAny || application.applicationTypeGateway || application.applicationTypeMicroservice) { - application.dockerServices.push('zipkin'); + dockerServices.push('zipkin'); } if (application.enableSwaggerCodegen) { - application.dockerServices.push('swagger-editor'); + dockerServices.push('swagger-editor'); } if (application.cacheProviderMemcached || application.cacheProviderRedis || application.cacheProviderHazelcast) { - application.dockerServices.push(application.cacheProvider); + dockerServices.push(application.cacheProvider!); } if ( application.databaseTypeCassandra || @@ -87,7 +87,7 @@ export default class DockerGenerator extends BaseApplicationGenerator { application.databaseTypeMongodb || application.databaseTypeNeo4j ) { - application.dockerServices.push(application.databaseType); + dockerServices.push(application.databaseType!); } if ( application.prodDatabaseTypePostgresql || @@ -95,7 +95,7 @@ export default class DockerGenerator extends BaseApplicationGenerator { application.prodDatabaseTypeMysql || application.prodDatabaseTypeMssql ) { - application.dockerServices.push(application.prodDatabaseType); + dockerServices.push(application.prodDatabaseType); } }, addAppServices({ application, source }) { @@ -172,13 +172,13 @@ export default class DockerGenerator extends BaseApplicationGenerator { get postWriting() { return this.asPostWritingTaskGroup({ async dockerServices({ application, source }) { - if (application.dockerServices.includes('cassandra')) { - const serviceName = application.databaseType; - source.addDockerExtendedServiceToApplicationAndServices( + if (application.dockerServices!.includes('cassandra')) { + const serviceName = application.databaseType!; + source.addDockerExtendedServiceToApplicationAndServices!( { serviceName }, { serviceFile: './cassandra.yml', serviceName: 'cassandra-migration' }, ); - source.addDockerDependencyToApplication( + source.addDockerDependencyToApplication!( { serviceName, condition: SERVICE_HEALTHY }, { serviceName: 'cassandra-migration', condition: SERVICE_COMPLETED_SUCCESSFULLY }, ); @@ -187,24 +187,24 @@ export default class DockerGenerator extends BaseApplicationGenerator { for (const serviceName of intersection(['postgresql', 'mysql', 'mariadb', 'mssql'], application.dockerServices)) { // Blank profile services starts if no profile is passed. const profiles = application.prodDatabaseType === application.devDatabaseType ? undefined : ['', 'prod']; - source.addDockerExtendedServiceToApplication({ serviceName }); - source.addDockerExtendedServiceToServices({ serviceName, additionalConfig: { profiles } }); - source.addDockerDependencyToApplication({ serviceName, condition: SERVICE_HEALTHY }); + source.addDockerExtendedServiceToApplication!({ serviceName }); + source.addDockerExtendedServiceToServices!({ serviceName, additionalConfig: { profiles } }); + source.addDockerDependencyToApplication!({ serviceName, condition: SERVICE_HEALTHY }); } for (const serviceName of intersection( ['couchbase', 'mongodb', 'neo4j', 'elasticsearch', 'keycloak'], application.dockerServices, )) { - source.addDockerExtendedServiceToApplicationAndServices({ serviceName }); - source.addDockerDependencyToApplication({ serviceName, condition: SERVICE_HEALTHY }); + source.addDockerExtendedServiceToApplicationAndServices!({ serviceName }); + source.addDockerDependencyToApplication!({ serviceName, condition: SERVICE_HEALTHY }); } - for (const serviceName of application.dockerServices.filter(service => ['redis', 'memcached', 'pulsar'].includes(service))) { - source.addDockerExtendedServiceToApplicationAndServices({ serviceName }); + for (const serviceName of application.dockerServices!.filter(service => ['redis', 'memcached', 'pulsar'].includes(service))) { + source.addDockerExtendedServiceToApplicationAndServices!({ serviceName }); } - if (application.dockerServices.includes('eureka')) { + if (application.dockerServices!.includes('eureka')) { const depends_on = application.authenticationTypeOauth2 ? { keycloak: { @@ -212,23 +212,23 @@ export default class DockerGenerator extends BaseApplicationGenerator { }, } : undefined; - source.addDockerExtendedServiceToApplicationAndServices({ + source.addDockerExtendedServiceToApplicationAndServices!({ serviceName: 'jhipster-registry', additionalConfig: { depends_on, }, }); - source.addDockerDependencyToApplication({ serviceName: 'jhipster-registry', condition: SERVICE_HEALTHY }); + source.addDockerDependencyToApplication!({ serviceName: 'jhipster-registry', condition: SERVICE_HEALTHY }); } - if (application.dockerServices.includes('consul')) { - source.addDockerExtendedServiceToApplicationAndServices( + if (application.dockerServices!.includes('consul')) { + source.addDockerExtendedServiceToApplicationAndServices!( { serviceName: 'consul' }, { serviceFile: './consul.yml', serviceName: 'consul-config-loader' }, ); } - if (application.dockerServices.includes('kafka')) { - source.addDockerExtendedServiceToApplicationAndServices({ serviceName: 'kafka' }); + if (application.dockerServices!.includes('kafka')) { + source.addDockerExtendedServiceToApplicationAndServices!({ serviceName: 'kafka' }); } }, @@ -302,6 +302,19 @@ export default class DockerGenerator extends BaseApplicationGenerator { return this.asPostWritingTaskGroup(this.delegateToBlueprint ? {} : this.postWriting); } + get end() { + return this.asEndTaskGroup({ + async dockerComposeUp({ control }) { + if (control.enviromentHasDockerCompose) { + this.log(''); + this.log.warn( + 'Docker Compose V2 is not installed on your computer. Some features may not work as expected. Read https://docs.docker.com/compose/install/', + ); + } + }, + }); + } + /** * @private * Returns the JDBC URL for a databaseType diff --git a/generators/docker/index.ts b/generators/docker/index.ts index 58ac334eafae..2d8013d19f51 100644 --- a/generators/docker/index.ts +++ b/generators/docker/index.ts @@ -16,4 +16,5 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +export { default as command } from './command.js'; export { default } from './generator.js'; diff --git a/generators/docker/support/docker-compose-file.ts b/generators/docker/support/docker-compose-file.ts index a0b7bbf94f40..a8d15583f963 100644 --- a/generators/docker/support/docker-compose-file.ts +++ b/generators/docker/support/docker-compose-file.ts @@ -16,6 +16,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import type { DockerComposeService } from '../types.js'; + /** * Creates EditFileCallback that creates a base docker compose yml file if empty. * @@ -28,7 +30,7 @@ export const createDockerComposeFile = ( name: ${name} `; -export const createDockerExtendedServices = (...services) => ({ +export const createDockerExtendedServices = (...services: DockerComposeService[]) => ({ services: Object.fromEntries( services.map(({ serviceName, serviceFile = `./${serviceName}.yml`, extendedServiceName = serviceName, additionalConfig = {} }) => [ serviceName, diff --git a/generators/docker/types.d.ts b/generators/docker/types.d.ts new file mode 100644 index 000000000000..4c0098339889 --- /dev/null +++ b/generators/docker/types.d.ts @@ -0,0 +1,18 @@ +export type DockerComposeService = { + serviceName: string; + serviceFile?: string; + condition?: string; + additionalConfig?: any; + extendedServiceName?: string; +}; + +export type DockerApplicationType = { + keycloakSecrets?: string[]; +}; + +export type DockerSourceType = { + addDockerExtendedServiceToApplicationAndServices?(...services: DockerComposeService[]): void; + addDockerExtendedServiceToServices?(...services: DockerComposeService[]): void; + addDockerExtendedServiceToApplication?(...services: DockerComposeService[]): void; + addDockerDependencyToApplication?(...services: DockerComposeService[]): void; +}; diff --git a/generators/java/generators/build-tool/generator.ts b/generators/java/generators/build-tool/generator.ts index bc349b9e4f09..a574d6914f1d 100644 --- a/generators/java/generators/build-tool/generator.ts +++ b/generators/java/generators/build-tool/generator.ts @@ -179,6 +179,9 @@ export default class BuildToolGenerator extends BaseApplicationGenerator { } }; }, + executable({ application }) { + application.buildToolExecutable = application.buildToolGradle ? 'gradlew' : 'mvnw'; + }, }); } diff --git a/generators/java/types.d.ts b/generators/java/types.d.ts index b869ddc9e321..185b7c7dd2b6 100644 --- a/generators/java/types.d.ts +++ b/generators/java/types.d.ts @@ -43,6 +43,7 @@ export type JavaNeedleOptions = GradleNeedleOptions; export type JavaApplication = JavaBootstrapStorageProperties & GradleApplication & { + buildToolExecutable: string; javaVersion: string; packageFolder: string; diff --git a/generators/server/support/database.ts b/generators/server/support/database.ts index f9e4a7142b57..2f0505a8b343 100644 --- a/generators/server/support/database.ts +++ b/generators/server/support/database.ts @@ -104,7 +104,7 @@ export const SQL_DB_OPTIONS = [ value: databaseTypes.MSSQL, name: 'Microsoft SQL Server', }, -]; +] as { value: string; name: string }[]; /** * Get DB type from DB value diff --git a/generators/spring-boot/generator.ts b/generators/spring-boot/generator.ts index 8ffe091f742a..99d98b24f80a 100644 --- a/generators/spring-boot/generator.ts +++ b/generators/spring-boot/generator.ts @@ -621,18 +621,32 @@ public void set${javaBeanCase(propertyName)}(${propertyType} ${propertyName}) { get end() { return this.asEndTaskGroup({ - end({ application }) { + end({ application, control }) { + const { buildToolExecutable } = application; this.log.ok('Spring Boot application generated successfully.'); - let executable = 'mvnw'; - if (application.buildToolGradle) { - executable = 'gradlew'; + if (application.dockerServices?.length && !control.enviromentHasDockerCompose) { + const dockerComposeCommand = chalk.yellow.bold('docker compose'); + this.log(''); + this.log + .warn(`${dockerComposeCommand} command was not found in your environment. The generated Spring Boot application uses ${dockerComposeCommand} integration by default. You can disable it by setting +${chalk.yellow.bold(` +spring: + docker: + compose: + enabled: false +`)} +in your ${chalk.yellow.bold(`${application.srcMainResources}config/application.yml`)} file or removing 'spring-boot-docker-compose' dependency. +`); } + let logMsgComment = ''; if (os.platform() === 'win32') { - logMsgComment = ` (${chalk.yellow.bold(executable)} if using Windows Command Prompt)`; + logMsgComment = ` (${chalk.yellow.bold(buildToolExecutable)} if using Windows Command Prompt)`; } - this.log.log(chalk.green(` Run your Spring Boot application:\n ${chalk.yellow.bold(`./${executable}`)}${logMsgComment}`)); + this.log.log( + chalk.green(` Run your Spring Boot application:\n ${chalk.yellow.bold(`./${buildToolExecutable}`)}${logMsgComment}`), + ); }, }); } diff --git a/generators/spring-boot/prompts.ts b/generators/spring-boot/prompts.ts index e4baa99593d7..dffa64901d51 100644 --- a/generators/spring-boot/prompts.ts +++ b/generators/spring-boot/prompts.ts @@ -42,9 +42,10 @@ const NO_DATABASE = databaseTypes.NO; const NO_CACHE_PROVIDER = cacheTypes.NO; const { GATLING, CUCUMBER } = testFrameworkTypes; -export async function askForServerSideOpts(this: CoreGenerator, { control }) { +export const askForServerSideOpts = asPromptingTask(async function ({ control }) { if (control.existingProject && !this.options.askAnswered) return; + const { enviromentHasDockerCompose } = control; const { applicationType, authenticationType, reactive } = this.jhipsterConfigWithDefaults; await this.prompt( @@ -105,17 +106,18 @@ export async function askForServerSideOpts(this: CoreGenerator, { control }) { type: 'list', name: 'devDatabaseType', message: `Which ${chalk.yellow('*development*')} database would you like to use?`, - choices: response => - [SQL_DB_OPTIONS.find(it => it.value === response.prodDatabaseType)].concat([ + choices: response => { + const currentDatabase = SQL_DB_OPTIONS.find(it => it.value === response.prodDatabaseType)!; + return [ { - value: H2_DISK, - name: 'H2 with disk-based persistence', + ...currentDatabase, + name: `${currentDatabase.name} (requires Docker or manually configured database)`, }, - { - value: H2_MEMORY, - name: 'H2 with in-memory persistence', - }, - ]) as any, + ].concat([ + { value: H2_DISK, name: `H2 with disk-based persistence` }, + { value: H2_MEMORY, name: `H2 with in-memory persistence` }, + ]); + }, default: this.jhipsterConfigWithDefaults.devDatabaseType, }, { @@ -168,7 +170,7 @@ export async function askForServerSideOpts(this: CoreGenerator, { control }) { ], this.config, ); -} +}); export const askForOptionalItems = asPromptingTask(async function askForOptionalItems(this: CoreGenerator, { control }) { if (control.existingProject && !this.options.askAnswered) return; diff --git a/lib/types/application/application.d.ts b/lib/types/application/application.d.ts index b2b12519e583..02ca08fe42fa 100644 --- a/lib/types/application/application.d.ts +++ b/lib/types/application/application.d.ts @@ -11,4 +11,7 @@ export type ApplicationType = BaseApplication & ExportApplicationPropertiesFromCommand & ExportApplicationPropertiesFromCommand; -export type BaseApplicationSource = SpringBootSourceType & ClientSourceType & LanguagesSource; +export type BaseApplicationSource = SpringBootSourceType & + ClientSourceType & + LanguagesSource & + import('../../../generators/docker/types.d.ts').DockerSourceType;