diff --git a/flow-plugins/flow-gradle-plugin/src/functionalTest/kotlin/com/vaadin/gradle/BuildWithoutLicenseTest.kt b/flow-plugins/flow-gradle-plugin/src/functionalTest/kotlin/com/vaadin/gradle/BuildWithoutLicenseTest.kt index a25b69a0b16..8cd708a7392 100644 --- a/flow-plugins/flow-gradle-plugin/src/functionalTest/kotlin/com/vaadin/gradle/BuildWithoutLicenseTest.kt +++ b/flow-plugins/flow-gradle-plugin/src/functionalTest/kotlin/com/vaadin/gradle/BuildWithoutLicenseTest.kt @@ -62,22 +62,20 @@ class BuildWithoutLicenseTest : AbstractGradleTest() { implementation("org.slf4j:slf4j-simple:$slf4jVersion") } - // Copy the flow-build-info.json so that tests can assert on it - // after the build. + // Copy the cached flow-build-info.json so that tests can assert + // on it after the build (the original is deleted by the task so + // IDE runs default to development mode). tasks.named('vaadinBuildFrontend').configure { doLast { - def mainResourcesDir = project.sourceSets.main.output.resourcesDir - - // Define source file path based on the resources directory - def sourceFile = new File(mainResourcesDir, "META-INF/VAADIN/config/flow-build-info.json") - - if (sourceFile.exists()) { + def cachedFile = new File(project.buildDir, "cached-flow-build-info.json") + + if (cachedFile.exists()) { def destFile = project.file("${buildInfo.absolutePath}") - destFile.text = sourceFile.text - - logger.lifecycle("Copied flow-build-info.json to temporary file: ${buildInfo.absolutePath}") + destFile.text = cachedFile.text + + logger.lifecycle("Copied cached flow-build-info.json to temporary file: ${buildInfo.absolutePath}") } else { - logger.warn("Could not find flow-build-info.json to copy") + logger.warn("Could not find cached flow-build-info.json to copy") } } } diff --git a/flow-plugins/flow-gradle-plugin/src/functionalTest/kotlin/com/vaadin/gradle/MiscMultiModuleTest.kt b/flow-plugins/flow-gradle-plugin/src/functionalTest/kotlin/com/vaadin/gradle/MiscMultiModuleTest.kt index 98cd7aabb79..0091eefd414 100644 --- a/flow-plugins/flow-gradle-plugin/src/functionalTest/kotlin/com/vaadin/gradle/MiscMultiModuleTest.kt +++ b/flow-plugins/flow-gradle-plugin/src/functionalTest/kotlin/com/vaadin/gradle/MiscMultiModuleTest.kt @@ -112,14 +112,14 @@ class MiscMultiModuleTest : AbstractGradleTest() { testProject.newFolder("web") val b: BuildResult = testProject.build("-Pvaadin.productionMode", "vaadinBuildFrontend", checkTasksSuccessful = false) - b.expectTaskSucceded("web:vaadinPrepareFrontend") + b.expectTaskNotRan("web:vaadinPrepareFrontend") b.expectTaskSucceded("web:vaadinBuildFrontend") expect(null) { b.task(":lib:vaadinPrepareFrontend") } expect(null) { b.task(":lib:vaadinBuildFrontend") } expect(null) { b.task(":vaadinPrepareFrontend") } expect(null) { b.task(":vaadinBuildFrontend") } - val tokenFile = File(testProject.dir, "web/build/resources/main/META-INF/VAADIN/config/flow-build-info.json") + val tokenFile = File(testProject.dir, "web/build/${VaadinBuildFrontendTask.CACHED_BUILD_INFO_FILE}") val tokenFileContent = JacksonUtils.readTree(tokenFile.readText()) expect("app-" + StringUtil.getHash("web", java.nio.charset.StandardCharsets.UTF_8 @@ -166,14 +166,14 @@ class MiscMultiModuleTest : AbstractGradleTest() { """.trimIndent()) val b: BuildResult = testProject.build("-Pvaadin.productionMode", "vaadinBuildFrontend", checkTasksSuccessful = false) - b.expectTaskSucceded("MY_APP_ID:vaadinPrepareFrontend") + b.expectTaskNotRan("MY_APP_ID:vaadinPrepareFrontend") b.expectTaskSucceded("MY_APP_ID:vaadinBuildFrontend") expect(null) { b.task(":lib:vaadinPrepareFrontend") } expect(null) { b.task(":lib:vaadinBuildFrontend") } expect(null) { b.task(":vaadinPrepareFrontend") } expect(null) { b.task(":vaadinBuildFrontend") } - val tokenFile = File(testProject.dir, "web/build/resources/main/META-INF/VAADIN/config/flow-build-info.json") + val tokenFile = File(testProject.dir, "web/build/${VaadinBuildFrontendTask.CACHED_BUILD_INFO_FILE}") val tokenFileContent = JacksonUtils.readTree(tokenFile.readText()) expect("app-" + StringUtil.getHash("MY_APP_ID", java.nio.charset.StandardCharsets.UTF_8 @@ -218,14 +218,14 @@ class MiscMultiModuleTest : AbstractGradleTest() { """.trimIndent()) val b: BuildResult = testProject.build("-Pvaadin.productionMode", "vaadinBuildFrontend", checkTasksSuccessful = false) - b.expectTaskSucceded("web:vaadinPrepareFrontend") + b.expectTaskNotRan("web:vaadinPrepareFrontend") b.expectTaskSucceded("web:vaadinBuildFrontend") expect(null) { b.task(":lib:vaadinPrepareFrontend") } expect(null) { b.task(":lib:vaadinBuildFrontend") } expect(null) { b.task(":vaadinPrepareFrontend") } expect(null) { b.task(":vaadinBuildFrontend") } - val tokenFile = File(testProject.dir, "web/build/resources/main/META-INF/VAADIN/config/flow-build-info.json") + val tokenFile = File(testProject.dir, "web/build/${VaadinBuildFrontendTask.CACHED_BUILD_INFO_FILE}") val tokenFileContent = JacksonUtils.readTree(tokenFile.readText()) expect("MY_APP_ID") { tokenFileContent.get(InitParameters.APPLICATION_IDENTIFIER).textValue() } } 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 3a766cc9359..8b90294a70d 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 @@ -69,8 +69,10 @@ class MiscSingleModuleTest : AbstractGradleTest() { @Test fun testWarProjectProductionMode() { doTestWarProjectProductionMode() - val tokenFile = File(testProject.dir, "build/resources/main/META-INF/VAADIN/config/flow-build-info.json") - val tokenFileContent = JacksonUtils.readTree(tokenFile.readText()) + // Read from archive since the token file is deleted from the + // filesystem after packaging to avoid stale production tokens + val tokenJson = testProject.builtWar.zipReadEntry("WEB-INF/classes/META-INF/VAADIN/config/flow-build-info.json") + val tokenFileContent = JacksonUtils.readTree(tokenJson!!) expect("app-" + StringUtil.getHash(testProject.dir.name, java.nio.charset.StandardCharsets.UTF_8 )) { tokenFileContent.get(InitParameters.APPLICATION_IDENTIFIER).textValue() } @@ -80,8 +82,8 @@ class MiscSingleModuleTest : AbstractGradleTest() { fun testWarProjectProductionModeWithCustomName() { testProject.settingsFile.writeText("rootProject.name = 'my-test-project'") doTestWarProjectProductionMode() - val tokenFile = File(testProject.dir, "build/resources/main/META-INF/VAADIN/config/flow-build-info.json") - val tokenFileContent = JacksonUtils.readTree(tokenFile.readText()) + val tokenJson = testProject.builtWar.zipReadEntry("WEB-INF/classes/META-INF/VAADIN/config/flow-build-info.json") + val tokenFileContent = JacksonUtils.readTree(tokenJson!!) expect("app-" + StringUtil.getHash("my-test-project", java.nio.charset.StandardCharsets.UTF_8 )) { tokenFileContent.get(InitParameters.APPLICATION_IDENTIFIER).textValue() } @@ -209,7 +211,7 @@ class MiscSingleModuleTest : AbstractGradleTest() { val build: BuildResult = testProject.build("-Pvaadin.productionMode", "build") - build.expectTaskSucceded("vaadinPrepareFrontend") + build.expectTaskNotRan("vaadinPrepareFrontend") build.expectTaskSucceded("vaadinBuildFrontend") val jar: File = testProject.builtJar @@ -255,7 +257,7 @@ class MiscSingleModuleTest : AbstractGradleTest() { val build: BuildResult = testProject.build("-Pvaadin.productionMode", "build") - build.expectTaskSucceded("vaadinPrepareFrontend") + build.expectTaskNotRan("vaadinPrepareFrontend") build.expectTaskSucceded("vaadinBuildFrontend") val jar: File = testProject.builtJar @@ -268,7 +270,7 @@ class MiscSingleModuleTest : AbstractGradleTest() { val build: BuildResult = testProject.build("bootJar") - build.expectTaskSucceded("vaadinPrepareFrontend") + build.expectTaskNotRan("vaadinPrepareFrontend") build.expectTaskSucceded("vaadinBuildFrontend") val jar: File = testProject.builtJar @@ -445,7 +447,7 @@ class MiscSingleModuleTest : AbstractGradleTest() { val build: BuildResult = testProject.build("-Pvaadin.productionMode", "build") - build.expectTaskSucceded("vaadinPrepareFrontend") + build.expectTaskNotRan("vaadinPrepareFrontend") build.expectTaskSucceded("vaadinBuildFrontend") val war: File = testProject.builtWar @@ -683,7 +685,7 @@ class MiscSingleModuleTest : AbstractGradleTest() { ) val build: BuildResult = testProject.build("build") - build.expectTaskSucceded("vaadinPrepareFrontend") + build.expectTaskNotRan("vaadinPrepareFrontend") build.expectTaskSucceded("vaadinBuildFrontend") val jar: File = testProject.builtJar @@ -779,38 +781,35 @@ class MiscSingleModuleTest : AbstractGradleTest() { """.trimIndent() ) - // First, run prepare and build to ensure everything works normally + // First, run build to ensure everything works normally val build1: BuildResult = testProject.build("-Pvaadin.productionMode", "build") - build1.expectTaskSucceded("vaadinPrepareFrontend") + build1.expectTaskNotRan("vaadinPrepareFrontend") build1.expectTaskSucceded("vaadinBuildFrontend") - // Verify token file was created - val tokenFile = File(testProject.dir, "build/resources/main/META-INF/VAADIN/config/flow-build-info.json") - expect(true) { tokenFile.exists() } - val tokenFileContent = JacksonUtils.readTree(tokenFile.readText()) + // Verify token file was packaged in the war archive (the token + // is deleted from the filesystem after packaging to prevent + // stale production tokens when running from an IDE) + val tokenJson = testProject.builtWar.zipReadEntry("WEB-INF/classes/META-INF/VAADIN/config/flow-build-info.json") + val tokenFileContent = JacksonUtils.readTree(tokenJson!!) expect("app-" + StringUtil.getHash(testProject.dir.name, java.nio.charset.StandardCharsets.UTF_8 )) { tokenFileContent.get(InitParameters.APPLICATION_IDENTIFIER).textValue() } - // Clean the token file to simulate it not existing - tokenFile.delete() - expect(false) { tokenFile.exists() } - - // Also delete the build-frontend marker file so that Gradle's - // up-to-date check detects a missing output and re-executes - // vaadinBuildFrontend (which contains the token file regeneration - // safety net). - val markerFile = File(testProject.dir, "build/vaadin-generated/build-frontend.marker") + // Delete the cached token file so that Gradle's up-to-date check + // detects a missing output and re-executes vaadinBuildFrontend. + val markerFile = File(testProject.dir, "build/cached-flow-build-info.json") markerFile.delete() - // Run vaadinBuildFrontend again - it should propagate build info - // even though the token file doesn't exist + // Run vaadinBuildFrontend directly (not via build/war) so the + // token file persists on disk for verification val build2: BuildResult = testProject.build("-Pvaadin.productionMode", "vaadinBuildFrontend") build2.expectTaskSucceded("vaadinBuildFrontend") - // Verify token file was re-created by propagation - expect(true) { tokenFile.exists() } - val newTokenFileContent = JacksonUtils.readTree(tokenFile.readText()) + // Verify token was re-created (read from the cached copy since + // the build service deletes the original after the build) + val cachedTokenFile = File(testProject.dir, "build/${VaadinBuildFrontendTask.CACHED_BUILD_INFO_FILE}") + expect(true) { cachedTokenFile.exists() } + val newTokenFileContent = JacksonUtils.readTree(cachedTokenFile.readText()) expect("app-" + StringUtil.getHash(testProject.dir.name, java.nio.charset.StandardCharsets.UTF_8 )) { newTokenFileContent.get(InitParameters.APPLICATION_IDENTIFIER).textValue() } 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 19073811aea..da191d365a8 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 @@ -102,6 +102,18 @@ private fun File.zipListAllFiles(): List = zin.fileNameSequence().toList() } +/** + * Reads the content of a single entry from this zip archive. + * @param entryPath the path inside the archive, e.g. `META-INF/VAADIN/config/flow-build-info.json` + * @return the entry content as a String, or null if not found + */ +fun File.zipReadEntry(entryPath: String): String? = + ZipInputStream(this.inputStream().buffered()).use { zin -> + generateSequence { zin.nextEntry } + .firstOrNull { it.name == entryPath } + ?.let { zin.readBytes().toString(Charsets.UTF_8) } + } + /** * Expects that given archive contains at least one file matching every glob in the [globs] list. * @param archiveProvider returns the zip file to examine. 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 4c70b187b21..6d1f90970ca 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 @@ -92,16 +92,19 @@ class VaadinSmokeTest : AbstractGradleTest() { @Test fun testBuildFrontendInProductionMode() { val result: BuildResult = testProject.build("-Pvaadin.productionMode", "vaadinBuildFrontend") - // vaadinBuildFrontend depends on vaadinPrepareFrontend - // let's explicitly check that vaadinPrepareFrontend has been run - result.expectTaskSucceded("vaadinPrepareFrontend") + // vaadinBuildFrontend is self-contained in production mode and + // performs its own frontend preparation without depending on + // vaadinPrepareFrontend + result.expectTaskNotRan("vaadinPrepareFrontend") val build = File(testProject.dir, "build/resources/main/META-INF/VAADIN/webapp/VAADIN/build") expect(true, build.toString()) { build.isDirectory } expect(true) { build.listFiles()!!.isNotEmpty() } build.find("*.br", 4..10) build.find("*.js", 4..10) - val tokenFile = File(testProject.dir, "build/resources/main/META-INF/VAADIN/config/flow-build-info.json") + // Read from cached copy since the task deletes the original + // token file so IDE runs default to development mode + val tokenFile = File(testProject.dir, "build/${VaadinBuildFrontendTask.CACHED_BUILD_INFO_FILE}") val buildInfo: JsonNode = JacksonUtils.readTree(tokenFile.readText()) expect(true, buildInfo.toString()) { buildInfo.get(InitParameters.SERVLET_PARAMETER_PRODUCTION_MODE).booleanValue() } expect("app-" + StringUtil.getHash(testProject.dir.name, @@ -112,11 +115,11 @@ class VaadinSmokeTest : AbstractGradleTest() { @Test fun testBuildFrontendInProductionMode_customApplicationIdentifier() { val result: BuildResult = testProject.build("-Pvaadin.applicationIdentifier=MY_APP_ID", "-Pvaadin.productionMode", "vaadinBuildFrontend", debug = true) - // vaadinBuildFrontend depends on vaadinPrepareFrontend - // let's explicitly check that vaadinPrepareFrontend has been run - result.expectTaskSucceded("vaadinPrepareFrontend") + // vaadinBuildFrontend is self-contained in production mode + result.expectTaskNotRan("vaadinPrepareFrontend") - val tokenFile = File(testProject.dir, "build/resources/main/META-INF/VAADIN/config/flow-build-info.json") + // Read from cached copy since the task deletes the original + val tokenFile = File(testProject.dir, "build/${VaadinBuildFrontendTask.CACHED_BUILD_INFO_FILE}") val buildInfo: JsonNode = JacksonUtils.readTree(tokenFile.readText()) expect("MY_APP_ID", buildInfo.toString()) { buildInfo.get(InitParameters.APPLICATION_IDENTIFIER).textValue() } } @@ -140,7 +143,7 @@ class VaadinSmokeTest : AbstractGradleTest() { """.trimIndent()) val result: BuildResult = testProject.build("-Pvaadin.productionMode", "build") - result.expectTaskSucceded("vaadinPrepareFrontend") + result.expectTaskNotRan("vaadinPrepareFrontend") result.expectTaskSucceded("vaadinBuildFrontend") val war = testProject.builtWar expect(true, "$war file doesn't exist") { war.isFile } @@ -328,9 +331,8 @@ class VaadinSmokeTest : AbstractGradleTest() { frontendDirectory = file("src/main/frontend") } """) - // let's explicitly check that vaadinPrepareFrontend has been run. val result: BuildResult = testProject.build("-Pvaadin.productionMode", "build") - result.expectTaskSucceded("vaadinPrepareFrontend") + result.expectTaskNotRan("vaadinPrepareFrontend") result.expectTaskSucceded("vaadinBuildFrontend") expect(false) { @@ -379,7 +381,7 @@ class VaadinSmokeTest : AbstractGradleTest() { """.trimIndent()) val result: BuildResult = testProject.build("-Pvaadin.productionMode", "build") - result.expectTaskSucceded("vaadinPrepareFrontend") + result.expectTaskNotRan("vaadinPrepareFrontend") result.expectTaskSucceded("vaadinBuildFrontend") val cssFile = File(testProject.dir, FrontendUtils.DEFAULT_PROJECT_FRONTEND_GENERATED_DIR + "jar-resources/mystyle.css") @@ -440,7 +442,7 @@ class VaadinSmokeTest : AbstractGradleTest() { ) val result: BuildResult = testProject.build("-Pvaadin.productionMode", "build", debug = true) - result.expectTaskSucceded("vaadinPrepareFrontend") + result.expectTaskNotRan("vaadinPrepareFrontend") result.expectTaskSucceded("vaadinBuildFrontend") val addonFile = diff --git a/flow-plugins/flow-gradle-plugin/src/main/kotlin/com/vaadin/gradle/BuildFrontendInputProperties.kt b/flow-plugins/flow-gradle-plugin/src/main/kotlin/com/vaadin/gradle/BuildFrontendInputProperties.kt index e35d4ce11d2..b72fb5148a3 100644 --- a/flow-plugins/flow-gradle-plugin/src/main/kotlin/com/vaadin/gradle/BuildFrontendInputProperties.kt +++ b/flow-plugins/flow-gradle-plugin/src/main/kotlin/com/vaadin/gradle/BuildFrontendInputProperties.kt @@ -111,10 +111,6 @@ internal class BuildFrontendInputProperties( fun getJavaResourceFolder(): Provider = config.javaResourceFolder.absolutePath - @Input - fun getGeneratedTsFolder(): Provider = - config.generatedTsFolder.absolutePath - @Input fun getPostInstallPackages(): ListProperty = config.postinstallPackages 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 c6ce2439bb3..4a30fc3d260 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 @@ -16,24 +16,53 @@ package com.vaadin.flow.gradle import java.io.File +import com.vaadin.flow.internal.FrontendUtils +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.OutputFile /** * Declaratively defines the outputs of the [VaadinBuildFrontendTask]. - * Uses a marker file in the Vaadin-generated directory to track build - * completion. The actual production bundle is written to the shared - * resources directory (build/resources/main/META-INF/VAADIN/) which - * cannot be declared as a task output because it overlaps with other - * Gradle tasks (e.g. processResources, Spring Boot's resolveMainClassName). + * + * A cached copy of the production `flow-build-info.json` token file is + * stored in the project build directory (e.g. `build/`). This serves + * both as the Gradle up-to-date marker (its existence and content drive + * the up-to-date check) and as a source for restoring the token when + * the original in `build/resources/main/` has been deleted (e.g. by + * post-packaging cleanup or deleteOnExit). + * + * The [getFrontendIndexHtml] output tracks the `index.html` file that the + * task creates if it is missing. Declaring it as an output means Gradle + * also tracks its content for up-to-date checking, so user edits to the + * file will trigger a rebuild. + * + * The generated frontend directory is declared as [LocalState] so that + * Gradle will clean it before re-execution and restore it from cache, + * but its contents do not participate in up-to-date checking (the + * generated files are non-deterministic across runs). */ internal class BuildFrontendOutputProperties( adapter: GradlePluginAdapter ) { - private val markerFile: File = - File(adapter.config.resourceOutputDirectory.get(), "build-frontend.marker") + private val cachedBuildInfoFile: File = + File(adapter.config.projectBuildDir.get(), + VaadinBuildFrontendTask.CACHED_BUILD_INFO_FILE) + private val generatedTsFolder: File = + BuildFrontendUtil.getGeneratedFrontendDirectory(adapter) + private val frontendIndexHtml: File = + File(BuildFrontendUtil.getFrontendDirectory(adapter), + FrontendUtils.INDEX_HTML) @OutputFile - fun getBuildFrontendMarker(): File = markerFile + fun getCachedBuildInfoFile(): File = cachedBuildInfoFile + + @OutputFile + @Optional + fun getFrontendIndexHtml(): File = frontendIndexHtml + + @LocalState + fun getGeneratedTsFolder(): File = generatedTsFolder } diff --git a/flow-plugins/flow-gradle-plugin/src/main/kotlin/com/vaadin/gradle/BuildFrontendTokenService.kt b/flow-plugins/flow-gradle-plugin/src/main/kotlin/com/vaadin/gradle/BuildFrontendTokenService.kt new file mode 100644 index 00000000000..4e50379ffac --- /dev/null +++ b/flow-plugins/flow-gradle-plugin/src/main/kotlin/com/vaadin/gradle/BuildFrontendTokenService.kt @@ -0,0 +1,76 @@ +/** + * Copyright 2000-2026 Vaadin Ltd. + * + * 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 + * + * http://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. + */ +package com.vaadin.flow.gradle + +import java.io.File +import org.gradle.api.provider.Property +import org.gradle.api.services.BuildService +import org.gradle.api.services.BuildServiceParameters + +/** + * A shared build service that manages the `flow-build-info.json` token + * file lifecycle during production builds. + * + * [ensureToken] restores the production token from a cached copy when + * the original has been deleted by a previous build's [close] call. + * This is called from jar/war `doFirst` so the token is available for + * packaging even when `vaadinBuildFrontend` is UP_TO_DATE. + * + * [close] is called by Gradle after all tasks that declared + * `usesService` have completed (including jar/war). It deletes the + * token so IDE runs default to development mode. + * + * The cached copy in the build directory + * ([VaadinBuildFrontendTask.CACHED_BUILD_INFO_FILE]) is written by the + * task action after a successful production build and persists across + * builds for restore purposes. + */ +internal abstract class BuildFrontendTokenService + : BuildService, AutoCloseable { + + interface Parameters : BuildServiceParameters { + /** Absolute path to the token file in build/resources/main/. */ + fun getTokenFilePath(): Property + /** Absolute path to the cached copy in build/. */ + fun getCachedTokenFilePath(): Property + } + + /** + * Restores the production token file from the cached copy if the + * original has been deleted (e.g. by a previous build's [close]). + */ + fun ensureToken() { + val tokenFile = File(parameters.getTokenFilePath().get()) + val cachedFile = File(parameters.getCachedTokenFilePath().get()) + if (!tokenFile.exists() && cachedFile.exists()) { + tokenFile.parentFile.mkdirs() + cachedFile.copyTo(tokenFile, overwrite = true) + } + } + + /** + * Called by Gradle after all tasks that declared `usesService` for + * this service have completed (including jar/war packaging tasks). + * Deletes the production token so IDE runs default to development + * mode. + */ + override fun close() { + val tokenFile = File(parameters.getTokenFilePath().get()) + if (tokenFile.exists()) { + tokenFile.delete() + } + } +} 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 0063ca17be5..f2246b070df 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 @@ -15,6 +15,7 @@ */ package com.vaadin.flow.gradle +import com.vaadin.flow.plugin.base.BuildFrontendUtil import org.gradle.api.GradleException import org.gradle.api.Plugin import org.gradle.api.Project @@ -66,16 +67,27 @@ public class FlowPlugin : Plugin { config.resourceOutputDirectory ) - // the processResources copies stuff from build/vaadin-generated - // (which is populated by this task) and therefore must run after vaadinPrepareFrontend task. - project.tasks.getByPath(config.processResourcesTaskName.get()).dependsOn("vaadinPrepareFrontend") - // auto-activate tasks: https://github.com/vaadin/vaadin-gradle-plugin/issues/48 if (config.productionMode.get()) { + // 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 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() + } } + } else { + // In development mode, processResources copies stuff from + // build/vaadin-generated (which is populated by + // vaadinPrepareFrontend) and therefore must run after it. + project.tasks.getByPath(config.processResourcesTaskName.get()).dependsOn("vaadinPrepareFrontend") } val toolsService = project.gradle.sharedServices.registerIfAbsent( @@ -98,12 +110,42 @@ public class FlowPlugin : Plugin { .doNotTrackState("State tracking is disabled. Use the 'alwaysExecutePrepareFrontend' plugin setting to enable the feature") } - project.tasks.getByName("vaadinBuildFrontend") - .usesService(toolsService) + // In production mode, vaadinBuildFrontend performs frontend + // preparation itself and needs dependent project jars to be + // built for classpath scanning to work properly. + val buildFrontendTask = project.tasks.getByName("vaadinBuildFrontend") + buildFrontendTask.dependsOn( + project.configurations.getByName(config.dependencyScope.get()).jars + ).usesService(toolsService) if (config.alwaysExecuteBuildFrontend.get()) { - project.tasks.getByName("vaadinBuildFrontend") + buildFrontendTask .doNotTrackState("State tracking is disabled. Use the 'alwaysExecuteBuildFrontend' plugin setting to enable the feature") } + + // Register a build service that restores the production token + // file before builds and cleans it up when the build finishes. + // The service is looked up by @ServiceReference on the task. + if (config.productionMode.get()) { + val buildAdapter = GradlePluginAdapter(buildFrontendTask, config, false) + val tokenService = project.gradle.sharedServices.registerIfAbsent( + "vaadinBuildFrontendToken", + BuildFrontendTokenService::class.java + ) { + it.parameters.getTokenFilePath().set( + BuildFrontendUtil.getTokenFile(buildAdapter).absolutePath + ) + it.parameters.getCachedTokenFilePath().set( + java.io.File(config.projectBuildDir.get(), + VaadinBuildFrontendTask.CACHED_BUILD_INFO_FILE).absolutePath + ) + } + // Ensure close() fires after vaadinBuildFrontend and + // all Jar/War packaging tasks have completed. + buildFrontendTask.usesService(tokenService) + project.tasks.withType(Jar::class.java) { task: Jar -> + task.usesService(tokenService) + } + } } } diff --git a/flow-plugins/flow-gradle-plugin/src/main/kotlin/com/vaadin/gradle/VaadinBuildFrontendTask.kt b/flow-plugins/flow-gradle-plugin/src/main/kotlin/com/vaadin/gradle/VaadinBuildFrontendTask.kt index 87fa2c0e757..b11e25237af 100644 --- a/flow-plugins/flow-gradle-plugin/src/main/kotlin/com/vaadin/gradle/VaadinBuildFrontendTask.kt +++ b/flow-plugins/flow-gradle-plugin/src/main/kotlin/com/vaadin/gradle/VaadinBuildFrontendTask.kt @@ -25,6 +25,7 @@ import com.vaadin.flow.server.frontend.TaskCleanFrontendFiles import com.vaadin.flow.server.frontend.scanner.FrontendDependenciesScanner import com.vaadin.flow.server.frontend.scanner.FrontendDependenciesScanner.FrontendDependenciesScannerFactory import com.vaadin.flow.internal.FrontendUtils +import com.vaadin.flow.server.InitParameters import com.vaadin.pro.licensechecker.LicenseChecker import com.vaadin.pro.licensechecker.MissingLicenseKeyException import org.gradle.api.DefaultTask @@ -66,9 +67,16 @@ import org.gradle.api.tasks.bundling.Jar @CacheableTask public abstract class VaadinBuildFrontendTask : DefaultTask() { + public companion object { + public const val CACHED_BUILD_INFO_FILE: String = "cached-flow-build-info.json" + } + @get:Internal internal abstract val adapter: Property + @ServiceReference("vaadinBuildFrontendToken") + internal abstract fun getTokenService(): Property + @ServiceReference internal abstract fun getSvc(): Property @@ -82,10 +90,11 @@ public abstract class VaadinBuildFrontendTask : DefaultTask() { /** * User-written frontend source files, excluding the `generated/` - * subdirectory. The `generated/` directory is excluded because it is - * an output of [VaadinPrepareFrontendTask] and also modified by this - * task's [vaadinBuildFrontend] action, which would make the inputs - * unstable across builds. + * subdirectory and `index.html`. The `generated/` directory is + * excluded because it is modified by this task's action. `index.html` + * is excluded because the task creates it when missing; it is tracked + * as an output instead so that user edits to it also invalidate the + * up-to-date check (Gradle tracks output file content). */ @get:InputFiles @get:Optional @@ -125,8 +134,6 @@ public abstract class VaadinBuildFrontendTask : DefaultTask() { group = "Vaadin" description = "Builds the frontend bundle with vite" - // we need the flow-build-info.json to be created, which is what the vaadinPrepareFrontend task does - dependsOn("vaadinPrepareFrontend") // Maven's task run in the LifecyclePhase.PROCESS_CLASSES phase // We need access to the produced classes, to be able to analyze e.g. @@ -145,11 +152,14 @@ public abstract class VaadinBuildFrontendTask : DefaultTask() { adapter.set(GradlePluginAdapter(this, config, false)) // Track user-written frontend source files, excluding the - // generated/ subdirectory which is modified by this task. + // generated/ subdirectory (modified by this task) and index.html + // (may be created by this task when missing; tracked as an output + // in BuildFrontendOutputProperties instead). frontendSourceFiles.from( config.effectiveFrontendDirectory.map { frontendDir -> project.fileTree(frontendDir) { it.exclude("generated/**") + it.exclude(FrontendUtils.INDEX_HTML) } } ) @@ -177,19 +187,19 @@ public abstract class VaadinBuildFrontendTask : DefaultTask() { .sortedBy { it.name } .joinToString("\n") { "${it.name}:${it.length()}" } }) + doFirst { + // Make sure the service is initialized, so its close method will be called at the end of the build. + getTokenService().get() + } } @TaskAction public fun vaadinBuildFrontend() { val config = adapter.get().config logger.info("Running the vaadinBuildFrontend task with effective configuration $config") - val tokenFile = BuildFrontendUtil.getTokenFile(adapter.get()) - if (!tokenFile.exists()) { - // if prepare-frontend token file doesn't exist, propagate build info - // to token file - logger.info("Token file does not exist, propagating build info") - BuildFrontendUtil.propagateBuildInfo(adapter.get()) - } + // Propagate build info to the token file so that runNodeUpdater + // and the frontend build have the configuration they need. + BuildFrontendUtil.propagateBuildInfo(adapter.get()) val options = Options(null, adapter.get().classFinder, config.npmFolder.get()) .withFrontendDirectory(BuildFrontendUtil.getFrontendDirectory(adapter.get())) @@ -238,10 +248,17 @@ public abstract class VaadinBuildFrontendTask : DefaultTask() { BuildFrontendUtil.updateBuildFile(adapter.get(), licenseRequired, commercialBannerRequired ) - // Write marker file for Gradle up-to-date tracking - val markerFile = outputProperties.get().getBuildFrontendMarker() - markerFile.parentFile.mkdirs() - markerFile.writeText("Build completed at ${System.currentTimeMillis()}") + // Cache the production token file and delete the original so + // that IDE runs default to development mode. Jar/War tasks + // restore the token from the cached copy in their doFirst + // action (via BuildFrontendTokenService.ensureToken()). + val tokenFile = BuildFrontendUtil.getTokenFile(adapter.get()) + val cachedTokenFile = outputProperties.get().getCachedBuildInfoFile() + cachedTokenFile.parentFile.mkdirs() + if (tokenFile.exists()) { + tokenFile.copyTo(cachedTokenFile, overwrite = true) + tokenFile.delete() + } } diff --git a/flow-plugins/flow-maven-plugin/src/main/java/com/vaadin/flow/plugin/maven/BuildFrontendMojo.java b/flow-plugins/flow-maven-plugin/src/main/java/com/vaadin/flow/plugin/maven/BuildFrontendMojo.java index ed38d578e15..bbe1fee71fd 100644 --- a/flow-plugins/flow-maven-plugin/src/main/java/com/vaadin/flow/plugin/maven/BuildFrontendMojo.java +++ b/flow-plugins/flow-maven-plugin/src/main/java/com/vaadin/flow/plugin/maven/BuildFrontendMojo.java @@ -212,6 +212,16 @@ protected void executeInternal() BuildFrontendUtil.updateBuildFile(this, licenseRequired, commercialBannerRequired); + // Schedule the token file for deletion when the JVM exits so + // that running the application from an IDE after a production + // build does not pick up a stale productionMode=true token. + // Maven goals always re-execute so this does not affect + // subsequent builds. + File tokenFile = BuildFrontendUtil.getTokenFile(this); + if (tokenFile.exists()) { + tokenFile.deleteOnExit(); + } + long ms = (System.nanoTime() - start) / 1000000; getLog().info("Build frontend completed in " + ms + " ms."); } diff --git a/flow-plugins/flow-plugin-base/src/main/java/com/vaadin/flow/plugin/base/BuildFrontendUtil.java b/flow-plugins/flow-plugin-base/src/main/java/com/vaadin/flow/plugin/base/BuildFrontendUtil.java index 1594193c466..a14c3db6e6c 100644 --- a/flow-plugins/flow-plugin-base/src/main/java/com/vaadin/flow/plugin/base/BuildFrontendUtil.java +++ b/flow-plugins/flow-plugin-base/src/main/java/com/vaadin/flow/plugin/base/BuildFrontendUtil.java @@ -913,7 +913,6 @@ public static void updateBuildFile(PluginAdapterBuild adapter, FileUtils.write(tokenFile, buildInfo.toPrettyString() + "\n", StandardCharsets.UTF_8.name()); - tokenFile.deleteOnExit(); } catch (IOException e) { adapter.logWarn("Unable to read token file", e); }