diff --git a/flow-plugins/flow-gradle-plugin/src/functionalTest/kotlin/com/vaadin/gradle/MiscSingleModuleTest.kt b/flow-plugins/flow-gradle-plugin/src/functionalTest/kotlin/com/vaadin/gradle/MiscSingleModuleTest.kt index f95c8246690..61c99c65c9e 100644 --- a/flow-plugins/flow-gradle-plugin/src/functionalTest/kotlin/com/vaadin/gradle/MiscSingleModuleTest.kt +++ b/flow-plugins/flow-gradle-plugin/src/functionalTest/kotlin/com/vaadin/gradle/MiscSingleModuleTest.kt @@ -867,6 +867,70 @@ class MiscSingleModuleTest : AbstractGradleTest() { "Skipping task ':vaadinBuildFrontend' as it is up-to-date") } } + @Test + fun buildFrontendBuildCacheRestoresProductionBundleForWar() { + testProject.buildFile.writeText( + """ + plugins { + id 'war' + id 'org.gretty' version '4.0.3' + id("com.vaadin.flow") + } + repositories { + mavenLocal() + mavenCentral() + maven { url = 'https://maven.vaadin.com/vaadin-prereleases' } + } + dependencies { + implementation("com.vaadin:flow:$flowVersion") + providedCompile("jakarta.servlet:jakarta.servlet-api:6.0.0") + implementation("org.slf4j:slf4j-simple:$slf4jVersion") + } + tasks.register('sourcesJar', Jar) { + archiveClassifier = 'sources' + from sourceSets.main.allSource + } + """.trimIndent() + ) + + var result = testProject.build("--build-cache", "-Pvaadin.productionMode", "build", "sourcesJar") + result.expectTaskSucceded("vaadinBuildFrontend") + expectArchiveContainsVaadinBundle(testProject.builtWar, false) + expectArchiveDoesntContainVaadinBundle( + testProject.folder("build/libs").find("*-sources.jar").first(), + false + ) + + File(testProject.dir, "build/vaadin-build-frontend").deleteRecursively() + File(testProject.dir, "build/cached-flow-build-info.json").delete() + File(testProject.dir, "build/libs").deleteRecursively() + + result = testProject.build("--build-cache", "-Pvaadin.productionMode", "build") + result.expectTaskOutcome("vaadinBuildFrontend", TaskOutcome.FROM_CACHE) + expectArchiveContainsVaadinBundle(testProject.builtWar, false) + } + + @Test + fun buildFrontendBuildCacheRestoresProductionBundleForSpringBootJar() { + doTestSpringProject() + + var result = testProject.build( + "--build-cache", "-Pvaadin.productionMode", "bootJar" + ) + result.expectTaskSucceded("vaadinBuildFrontend") + expectArchiveContainsVaadinBundle(testProject.builtJar, true) + + File(testProject.dir, "build/vaadin-build-frontend").deleteRecursively() + File(testProject.dir, "build/cached-flow-build-info.json").delete() + File(testProject.dir, "build/libs").deleteRecursively() + + result = testProject.build( + "--build-cache", "-Pvaadin.productionMode", "bootJar" + ) + result.expectTaskOutcome("vaadinBuildFrontend", TaskOutcome.FROM_CACHE) + expectArchiveContainsVaadinBundle(testProject.builtJar, true) + } + @Test fun buildFrontendIncrementalBuilds_rerunsOnInputChange() { testProject.buildFile.writeText( diff --git a/flow-plugins/flow-gradle-plugin/src/functionalTest/kotlin/com/vaadin/gradle/TestUtils.kt b/flow-plugins/flow-gradle-plugin/src/functionalTest/kotlin/com/vaadin/gradle/TestUtils.kt index db77f9c9477..58a296bcd6b 100644 --- a/flow-plugins/flow-gradle-plugin/src/functionalTest/kotlin/com/vaadin/gradle/TestUtils.kt +++ b/flow-plugins/flow-gradle-plugin/src/functionalTest/kotlin/com/vaadin/gradle/TestUtils.kt @@ -153,6 +153,7 @@ fun expectArchiveContainsVaadinBundle( val isStandaloneJar: Boolean = !isWar && !isSpringBootJar val resourcePackaging: String = when { isWar -> "WEB-INF/classes/" + isSpringBootJar -> "BOOT-INF/classes/" else -> "" } expectArchiveContains( diff --git a/flow-plugins/flow-gradle-plugin/src/functionalTest/kotlin/com/vaadin/gradle/VaadinSmokeTest.kt b/flow-plugins/flow-gradle-plugin/src/functionalTest/kotlin/com/vaadin/gradle/VaadinSmokeTest.kt index b0a5f3ab713..728cec02f66 100644 --- a/flow-plugins/flow-gradle-plugin/src/functionalTest/kotlin/com/vaadin/gradle/VaadinSmokeTest.kt +++ b/flow-plugins/flow-gradle-plugin/src/functionalTest/kotlin/com/vaadin/gradle/VaadinSmokeTest.kt @@ -85,7 +85,7 @@ class VaadinSmokeTest : AbstractGradleTest() { result.expectTaskNotRan("vaadinPrepareFrontend") result.expectTaskNotRan("vaadinBuildFrontend") - val build = File(testProject.dir, "build/resources/main/META-INF/VAADIN/webapp/VAADIN/build") + val build = File(testProject.dir, "build/vaadin-build-frontend/META-INF/VAADIN/webapp/VAADIN/build") expect(false, build.toString()) { build.exists() } } @@ -97,7 +97,7 @@ class VaadinSmokeTest : AbstractGradleTest() { // vaadinPrepareFrontend result.expectTaskNotRan("vaadinPrepareFrontend") - val build = File(testProject.dir, "build/resources/main/META-INF/VAADIN/webapp/VAADIN/build") + val build = File(testProject.dir, "build/vaadin-build-frontend/META-INF/VAADIN/webapp/VAADIN/build") expect(true, build.toString()) { build.isDirectory } expect(true) { build.listFiles()!!.isNotEmpty() } build.find("*.br", 4..10) diff --git a/flow-plugins/flow-gradle-plugin/src/main/kotlin/com/vaadin/gradle/BuildFrontendOutputProperties.kt b/flow-plugins/flow-gradle-plugin/src/main/kotlin/com/vaadin/gradle/BuildFrontendOutputProperties.kt index 4a30fc3d260..e21b7810fa8 100644 --- a/flow-plugins/flow-gradle-plugin/src/main/kotlin/com/vaadin/gradle/BuildFrontendOutputProperties.kt +++ b/flow-plugins/flow-gradle-plugin/src/main/kotlin/com/vaadin/gradle/BuildFrontendOutputProperties.kt @@ -21,6 +21,7 @@ import com.vaadin.flow.plugin.base.BuildFrontendUtil import org.gradle.api.provider.Property import org.gradle.api.tasks.LocalState import org.gradle.api.tasks.Optional +import org.gradle.api.tasks.OutputDirectory import org.gradle.api.tasks.OutputFile /** @@ -52,6 +53,8 @@ internal class BuildFrontendOutputProperties( VaadinBuildFrontendTask.CACHED_BUILD_INFO_FILE) private val generatedTsFolder: File = BuildFrontendUtil.getGeneratedFrontendDirectory(adapter) + private val servletResourceOutputDirectory: File = + adapter.servletResourceOutputDirectory() private val frontendIndexHtml: File = File(BuildFrontendUtil.getFrontendDirectory(adapter), FrontendUtils.INDEX_HTML) @@ -63,6 +66,10 @@ internal class BuildFrontendOutputProperties( @Optional fun getFrontendIndexHtml(): File = frontendIndexHtml + @OutputDirectory + fun getServletResourceOutputDirectory(): File = + servletResourceOutputDirectory + @LocalState fun getGeneratedTsFolder(): File = generatedTsFolder } diff --git a/flow-plugins/flow-gradle-plugin/src/main/kotlin/com/vaadin/gradle/FlowPlugin.kt b/flow-plugins/flow-gradle-plugin/src/main/kotlin/com/vaadin/gradle/FlowPlugin.kt index 93024da4726..5981732c5b8 100644 --- a/flow-plugins/flow-gradle-plugin/src/main/kotlin/com/vaadin/gradle/FlowPlugin.kt +++ b/flow-plugins/flow-gradle-plugin/src/main/kotlin/com/vaadin/gradle/FlowPlugin.kt @@ -21,6 +21,7 @@ import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.api.plugins.JavaPlugin import org.gradle.api.tasks.bundling.Jar +import org.gradle.api.tasks.bundling.War import org.gradle.util.GradleVersion /** @@ -72,15 +73,37 @@ public class FlowPlugin : Plugin { // In production mode, vaadinBuildFrontend is self-contained // and performs its own frontend preparation, so there is no // need for vaadinPrepareFrontend to run beforehand. - // this will also catch the War task since it extends from Jar + val buildFrontendTask = project.tasks.getByName("vaadinBuildFrontend") + val buildAdapter = GradlePluginAdapter(buildFrontendTask, config, false) + val vaadinServletResourcesDirectory = + buildAdapter.servletResourceOutputDirectory() + val vaadinBuildFrontendOutputDirectory = + vaadinServletResourcesDirectory.parentFile?.parentFile + + val sourceSetResourcesDirectory = + project.getBuildResourcesDir(config.sourceSetName.get()) + project.tasks.withType(Jar::class.java) { task: Jar -> - task.dependsOn("vaadinBuildFrontend") - // Restore the production token before packaging in - // case it was deleted by a previous build's cleanup. - task.doFirst { - val svc = (project.tasks.getByName("vaadinBuildFrontend") - as VaadinBuildFrontendTask).getTokenService().orNull - svc?.ensureToken() + if (task.isVaadinApplicationArchiveTask()) { + task.dependsOn("vaadinBuildFrontend") + if (vaadinBuildFrontendOutputDirectory != null && + vaadinBuildFrontendOutputDirectory.canonicalFile != + sourceSetResourcesDirectory.canonicalFile + ) { + task.from(vaadinBuildFrontendOutputDirectory) { + task.vaadinBuildFrontendResourcesArchivePath() + ?.let { path -> + it.into(path) + } + } + } + // Restore the production token before packaging in + // case it was deleted by a previous build's cleanup. + task.doFirst { + val svc = (buildFrontendTask + as VaadinBuildFrontendTask).getTokenService().orNull + svc?.ensureToken() + } } } } else if (config.alwaysExecutePrepareFrontend.get()) { @@ -149,7 +172,9 @@ public class FlowPlugin : Plugin { // all Jar/War packaging tasks have completed. buildFrontendTask.usesService(tokenService) project.tasks.withType(Jar::class.java) { task: Jar -> - task.usesService(tokenService) + if (task.isVaadinApplicationArchiveTask()) { + task.usesService(tokenService) + } } } } @@ -166,4 +191,21 @@ public class FlowPlugin : Plugin { ) } } + + private fun Jar.vaadinBuildFrontendResourcesArchivePath(): String? { + return when { + this is War -> "WEB-INF/classes" + isSpringBootJar() -> "BOOT-INF/classes" + else -> null + } + } + + private fun Jar.isVaadinApplicationArchiveTask(): Boolean = + name == JavaPlugin.JAR_TASK_NAME || this is War || isSpringBootJar() + + private fun Jar.isSpringBootJar(): Boolean = + generateSequence(javaClass as Class<*>) { it.superclass } + .any { + it.name == "org.springframework.boot.gradle.tasks.bundling.BootJar" + } } diff --git a/flow-plugins/flow-gradle-plugin/src/main/kotlin/com/vaadin/gradle/GradlePluginAdapter.kt b/flow-plugins/flow-gradle-plugin/src/main/kotlin/com/vaadin/gradle/GradlePluginAdapter.kt index 2666526f9f6..d15e8531395 100644 --- a/flow-plugins/flow-gradle-plugin/src/main/kotlin/com/vaadin/gradle/GradlePluginAdapter.kt +++ b/flow-plugins/flow-gradle-plugin/src/main/kotlin/com/vaadin/gradle/GradlePluginAdapter.kt @@ -47,7 +47,7 @@ internal class GradlePluginAdapter private constructor( private val projectDir = config.projectDir private val projectName = config.projectName - private val buildResourcesDir: File = + private val buildResourcesDir = project.getBuildResourcesDir(config.sourceSetName.get()) private val jarProject: Boolean = project.tasks.withType(War::class.java).isEmpty() @@ -234,14 +234,19 @@ internal class GradlePluginAdapter private constructor( // generate stuff to build/vaadin-generated. // // However, after processResources is done, anything generated into - // build/vaadin-generated would simply be ignored. In such case we therefore - // need to generate stuff directly to build/resources/main. + // build/vaadin-generated would simply be ignored. In such cases, + // production resources must either be generated to the task-owned + // frontend output tree or to the source set resources directory. if (isBeforeProcessResources) { return File( config.resourceOutputDirectory.get(), Constants.VAADIN_SERVLET_RESOURCES ) } + val frontendOutputDirectory = frontendOutputDirectory() + if (frontendOutputDirectory.hasVaadinWebappResourcesPath()) { + return frontendOutputDirectory.parentFile + } return File(buildResourcesDir, Constants.VAADIN_SERVLET_RESOURCES) } @@ -261,6 +266,11 @@ internal class GradlePluginAdapter private constructor( override fun generateEmbeddableWebComponents(): Boolean = config.generateEmbeddableWebComponents.get() + private fun File.hasVaadinWebappResourcesPath(): Boolean = + path.replace(File.separatorChar, '/').removeSuffix("/").endsWith( + Constants.VAADIN_WEBAPP_RESOURCES.removeSuffix("/") + ) + override fun optimizeBundle(): Boolean = config.optimizeBundle.get() override fun runNpmInstall(): Boolean = config.runNpmInstall.get() diff --git a/flow-plugins/flow-gradle-plugin/src/main/kotlin/com/vaadin/gradle/VaadinFlowPluginExtension.kt b/flow-plugins/flow-gradle-plugin/src/main/kotlin/com/vaadin/gradle/VaadinFlowPluginExtension.kt index 8b657ed7d50..857811f6d11 100644 --- a/flow-plugins/flow-gradle-plugin/src/main/kotlin/com/vaadin/gradle/VaadinFlowPluginExtension.kt +++ b/flow-plugins/flow-gradle-plugin/src/main/kotlin/com/vaadin/gradle/VaadinFlowPluginExtension.kt @@ -47,8 +47,8 @@ public abstract class VaadinFlowPluginExtension @Inject constructor(private val /** * The folder where the frontend build tool should output index.js and other generated - * files. Defaults to `null` which will use the auto-detected value of - * resoucesDir of the main SourceSet, usually `build/resources/main/META-INF/VAADIN/webapp/`. + * files. Defaults to `null` which will use a task-owned build directory, + * usually `build/vaadin-build-frontend/META-INF/VAADIN/webapp/`. */ @Deprecated( "use frontendOutputDirectory instead", @@ -58,8 +58,8 @@ public abstract class VaadinFlowPluginExtension @Inject constructor(private val /** * The folder where the frontend build tool should output index.js and other generated - * files. Defaults to `null` which will use the auto-detected value of - * resoucesDir of the main SourceSet, usually `build/resources/main/META-INF/VAADIN/webapp/`. + * files. Defaults to `null` which will use a task-owned build directory, + * usually `build/vaadin-build-frontend/META-INF/VAADIN/webapp/`. */ public abstract val frontendOutputDirectory: Property @@ -419,12 +419,14 @@ public class PluginEffectiveConfiguration( extension.frontendOutputDirectory.convention( extension.webpackOutputDirectory .convention( - sourceSetName.map { - File( - project.getBuildResourcesDir(it), - Constants.VAADIN_WEBAPP_RESOURCES - ) - } + project.layout.buildDirectory + .dir("vaadin-build-frontend") + .map { + File( + it.asFile, + Constants.VAADIN_WEBAPP_RESOURCES + ) + } ) )