From 96ce543fb82ac0a1ce4d346d28e26fdb675afa62 Mon Sep 17 00:00:00 2001 From: "Ben St. Pierre" Date: Sun, 10 May 2026 00:49:01 -0600 Subject: [PATCH 1/2] fix: restore Gradle cached Vaadin frontend outputs --- .../com/vaadin/gradle/MiscSingleModuleTest.kt | 35 +++++++++++++++++++ .../com/vaadin/gradle/VaadinSmokeTest.kt | 4 +-- .../gradle/BuildFrontendOutputProperties.kt | 7 ++++ .../kotlin/com/vaadin/gradle/FlowPlugin.kt | 24 ++++++++++++- .../com/vaadin/gradle/GradlePluginAdapter.kt | 4 +-- .../gradle/VaadinFlowPluginExtension.kt | 14 ++++---- 6 files changed, 76 insertions(+), 12 deletions(-) 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..c8ae8a9c0ff 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,41 @@ 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") + } + """.trimIndent() + ) + + var result = testProject.build("--build-cache", "-Pvaadin.productionMode", "build") + result.expectTaskSucceded("vaadinBuildFrontend") + expectArchiveContainsVaadinBundle(testProject.builtWar, 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 buildFrontendIncrementalBuilds_rerunsOnInputChange() { testProject.buildFile.writeText( 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..14690f54a70 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,13 +73,25 @@ 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. + val buildFrontendTask = project.tasks.getByName("vaadinBuildFrontend") + val buildAdapter = GradlePluginAdapter(buildFrontendTask, config, false) + val vaadinServletResourcesDirectory = + buildAdapter.servletResourceOutputDirectory() + val vaadinBuildFrontendOutputDirectory = + vaadinServletResourcesDirectory.parentFile.parentFile + // this will also catch the War task since it extends from Jar project.tasks.withType(Jar::class.java) { task: Jar -> task.dependsOn("vaadinBuildFrontend") + 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 = (project.tasks.getByName("vaadinBuildFrontend") + val svc = (buildFrontendTask as VaadinBuildFrontendTask).getTokenService().orNull svc?.ensureToken() } @@ -166,4 +179,13 @@ public class FlowPlugin : Plugin { ) } } + + private fun Jar.vaadinBuildFrontendResourcesArchivePath(): String? { + return when { + this is War -> "WEB-INF/classes" + javaClass.name == "org.springframework.boot.gradle.tasks.bundling.BootJar" -> + "BOOT-INF/classes" + else -> null + } + } } 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..252790f979b 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,8 +47,6 @@ internal class GradlePluginAdapter private constructor( private val projectDir = config.projectDir private val projectName = config.projectName - private val buildResourcesDir: File = - project.getBuildResourcesDir(config.sourceSetName.get()) private val jarProject: Boolean = project.tasks.withType(War::class.java).isEmpty() private val jarFiles: FileCollection @@ -242,7 +240,7 @@ internal class GradlePluginAdapter private constructor( Constants.VAADIN_SERVLET_RESOURCES ) } - return File(buildResourcesDir, Constants.VAADIN_SERVLET_RESOURCES) + return frontendOutputDirectory().parentFile } override fun webpackOutputDirectory(): File = frontendOutputDirectory() 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..69e043065cc 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 @@ -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 + ) + } ) ) From 62c56fe05a434008ad2cdd293cbeac2088aae39e Mon Sep 17 00:00:00 2001 From: "Ben St. Pierre" Date: Sun, 10 May 2026 01:20:23 -0600 Subject: [PATCH 2/2] fix: harden Gradle frontend archive packaging --- .../com/vaadin/gradle/MiscSingleModuleTest.kt | 31 ++++++++++- .../kotlin/com/vaadin/gradle/TestUtils.kt | 1 + .../kotlin/com/vaadin/gradle/FlowPlugin.kt | 52 +++++++++++++------ .../com/vaadin/gradle/GradlePluginAdapter.kt | 18 +++++-- .../gradle/VaadinFlowPluginExtension.kt | 8 +-- 5 files changed, 86 insertions(+), 24 deletions(-) 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 c8ae8a9c0ff..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 @@ -886,12 +886,20 @@ class MiscSingleModuleTest : AbstractGradleTest() { 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") + 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() @@ -902,6 +910,27 @@ class MiscSingleModuleTest : AbstractGradleTest() { 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/main/kotlin/com/vaadin/gradle/FlowPlugin.kt b/flow-plugins/flow-gradle-plugin/src/main/kotlin/com/vaadin/gradle/FlowPlugin.kt index 14690f54a70..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 @@ -78,22 +78,32 @@ public class FlowPlugin : Plugin { val vaadinServletResourcesDirectory = buildAdapter.servletResourceOutputDirectory() val vaadinBuildFrontendOutputDirectory = - vaadinServletResourcesDirectory.parentFile.parentFile + vaadinServletResourcesDirectory.parentFile?.parentFile + + val sourceSetResourcesDirectory = + project.getBuildResourcesDir(config.sourceSetName.get()) - // this will also catch the War task since it extends from Jar project.tasks.withType(Jar::class.java) { task: Jar -> - task.dependsOn("vaadinBuildFrontend") - task.from(vaadinBuildFrontendOutputDirectory) { - task.vaadinBuildFrontendResourcesArchivePath()?.let { path -> - it.into(path) + 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() } - } - // 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()) { @@ -162,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) + } } } } @@ -183,9 +195,17 @@ public class FlowPlugin : Plugin { private fun Jar.vaadinBuildFrontendResourcesArchivePath(): String? { return when { this is War -> "WEB-INF/classes" - javaClass.name == "org.springframework.boot.gradle.tasks.bundling.BootJar" -> - "BOOT-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 252790f979b..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,6 +47,8 @@ internal class GradlePluginAdapter private constructor( private val projectDir = config.projectDir private val projectName = config.projectName + private val buildResourcesDir = + project.getBuildResourcesDir(config.sourceSetName.get()) private val jarProject: Boolean = project.tasks.withType(War::class.java).isEmpty() private val jarFiles: FileCollection @@ -232,15 +234,20 @@ 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 ) } - return frontendOutputDirectory().parentFile + val frontendOutputDirectory = frontendOutputDirectory() + if (frontendOutputDirectory.hasVaadinWebappResourcesPath()) { + return frontendOutputDirectory.parentFile + } + return File(buildResourcesDir, Constants.VAADIN_SERVLET_RESOURCES) } override fun webpackOutputDirectory(): File = frontendOutputDirectory() @@ -259,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 69e043065cc..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