Skip to content

Commit 88408e7

Browse files
authored
feat: BuildFrontend Incremental build (#23884)
Add incremental build to the buildFrontend task. Closes #17354
1 parent 7891d15 commit 88408e7

File tree

8 files changed

+532
-6
lines changed

8 files changed

+532
-6
lines changed

flow-plugins/flow-gradle-plugin/src/functionalTest/kotlin/com/vaadin/gradle/MiscSingleModuleTest.kt

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -796,6 +796,13 @@ class MiscSingleModuleTest : AbstractGradleTest() {
796796
tokenFile.delete()
797797
expect(false) { tokenFile.exists() }
798798

799+
// Also delete the build-frontend marker file so that Gradle's
800+
// up-to-date check detects a missing output and re-executes
801+
// vaadinBuildFrontend (which contains the token file regeneration
802+
// safety net).
803+
val markerFile = File(testProject.dir, "build/vaadin-generated/build-frontend.marker")
804+
markerFile.delete()
805+
799806
// Run vaadinBuildFrontend again - it should propagate build info
800807
// even though the token file doesn't exist
801808
val build2: BuildResult = testProject.build("-Pvaadin.productionMode", "vaadinBuildFrontend")
@@ -808,4 +815,126 @@ class MiscSingleModuleTest : AbstractGradleTest() {
808815
java.nio.charset.StandardCharsets.UTF_8
809816
)) { newTokenFileContent.get(InitParameters.APPLICATION_IDENTIFIER).textValue() }
810817
}
818+
819+
@Test
820+
fun buildFrontendIncrementalBuilds_featureEnabled() {
821+
testProject.buildFile.writeText(
822+
"""
823+
plugins {
824+
id 'war'
825+
id 'org.gretty' version '4.0.3'
826+
id("com.vaadin.flow")
827+
}
828+
repositories {
829+
mavenLocal()
830+
mavenCentral()
831+
maven { url = 'https://maven.vaadin.com/vaadin-prereleases' }
832+
}
833+
dependencies {
834+
implementation("com.vaadin:flow:$flowVersion")
835+
// Uncomment for faster test.
836+
// implementation("com.vaadin:vaadin-prod-bundle:$flowVersion")
837+
providedCompile("jakarta.servlet:jakarta.servlet-api:6.0.0")
838+
implementation("org.slf4j:slf4j-simple:$slf4jVersion")
839+
}
840+
""".trimIndent()
841+
)
842+
var result = testProject.build("-Pvaadin.productionMode", "vaadinBuildFrontend", debug = true)
843+
expect(true) { result.output.contains(
844+
"Task ':vaadinBuildFrontend' is not up-to-date") }
845+
846+
result = testProject.build("-Pvaadin.productionMode", "vaadinBuildFrontend", debug = true, checkTasksSuccessful = false)
847+
result.expectTaskOutcome("vaadinBuildFrontend", TaskOutcome.UP_TO_DATE)
848+
expect(true) { result.output.contains(
849+
"Skipping task ':vaadinBuildFrontend' as it is up-to-date") }
850+
}
851+
852+
@Test
853+
fun buildFrontendIncrementalBuilds_rerunsOnInputChange() {
854+
testProject.buildFile.writeText(
855+
"""
856+
plugins {
857+
id 'war'
858+
id 'org.gretty' version '4.0.3'
859+
id("com.vaadin.flow")
860+
}
861+
repositories {
862+
mavenLocal()
863+
mavenCentral()
864+
maven { url = 'https://maven.vaadin.com/vaadin-prereleases' }
865+
}
866+
dependencies {
867+
implementation("com.vaadin:flow:$flowVersion")
868+
providedCompile("jakarta.servlet:jakarta.servlet-api:6.0.0")
869+
implementation("org.slf4j:slf4j-simple:$slf4jVersion")
870+
}
871+
""".trimIndent()
872+
)
873+
testProject.build("-Pvaadin.productionMode", "vaadinBuildFrontend", debug = true)
874+
875+
// Second run should be up-to-date
876+
var result = testProject.build("-Pvaadin.productionMode", "vaadinBuildFrontend", debug = true, checkTasksSuccessful = false)
877+
result.expectTaskOutcome("vaadinBuildFrontend", TaskOutcome.UP_TO_DATE)
878+
879+
// Change a config input (optimizeBundle) to trigger re-execution
880+
testProject.buildFile.writeText(
881+
"""
882+
plugins {
883+
id 'war'
884+
id 'org.gretty' version '4.0.3'
885+
id("com.vaadin.flow")
886+
}
887+
repositories {
888+
mavenLocal()
889+
mavenCentral()
890+
maven { url = 'https://maven.vaadin.com/vaadin-prereleases' }
891+
}
892+
dependencies {
893+
implementation("com.vaadin:flow:$flowVersion")
894+
providedCompile("jakarta.servlet:jakarta.servlet-api:6.0.0")
895+
implementation("org.slf4j:slf4j-simple:$slf4jVersion")
896+
}
897+
vaadin {
898+
optimizeBundle = false
899+
}
900+
""".trimIndent()
901+
)
902+
result = testProject.build("-Pvaadin.productionMode", "vaadinBuildFrontend", debug = true)
903+
expect(true) { result.output.contains(
904+
"Task ':vaadinBuildFrontend' is not up-to-date") }
905+
}
906+
907+
@Test
908+
fun buildFrontendIncrementalBuilds_disableWithProperty() {
909+
testProject.buildFile.writeText(
910+
"""
911+
plugins {
912+
id 'war'
913+
id 'org.gretty' version '4.0.3'
914+
id("com.vaadin.flow")
915+
}
916+
repositories {
917+
mavenLocal()
918+
mavenCentral()
919+
maven { url = 'https://maven.vaadin.com/vaadin-prereleases' }
920+
}
921+
dependencies {
922+
implementation("com.vaadin:flow:$flowVersion")
923+
providedCompile("jakarta.servlet:jakarta.servlet-api:6.0.0")
924+
implementation("org.slf4j:slf4j-simple:$slf4jVersion")
925+
}
926+
vaadin {
927+
alwaysExecuteBuildFrontend = true
928+
}
929+
""".trimIndent()
930+
)
931+
repeat(3) {
932+
val result = testProject.build("-Pvaadin.productionMode", "vaadinBuildFrontend", debug = true)
933+
expect(true) {
934+
result.output.contains(
935+
"Task ':vaadinBuildFrontend' is not up-to-date"
936+
)
937+
}
938+
}
939+
}
811940
}

flow-plugins/flow-gradle-plugin/src/functionalTest/kotlin/com/vaadin/gradle/VaadinSmokeTest.kt

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -637,6 +637,74 @@ class VaadinSmokeTest : AbstractGradleTest() {
637637
) { vaadinReactTsx.exists() }
638638
}
639639

640+
@Test
641+
fun testBuildFrontend_configurationCache() {
642+
// Create frontend folder, that will otherwise be created by the first
643+
// execution, invalidating the cache on the second run
644+
testProject.newFolder("src/main/frontend")
645+
646+
val result = testProject.build("--configuration-cache", "-Pvaadin.productionMode", "vaadinBuildFrontend")
647+
result.expectTaskSucceded("vaadinBuildFrontend")
648+
assertContains(result.output, "Calculating task graph as no cached configuration is available for tasks: vaadinBuildFrontend")
649+
assertContains(result.output, "Configuration cache entry stored")
650+
651+
val result2 = testProject.build("--configuration-cache", "-Pvaadin.productionMode", "vaadinBuildFrontend", checkTasksSuccessful = false)
652+
result2.expectTaskOutcome("vaadinBuildFrontend", TaskOutcome.UP_TO_DATE)
653+
assertContains(result2.output, "Reusing configuration cache")
654+
}
655+
656+
@Test
657+
fun testBuildFrontend_configurationCache_configurationChange_cacheInvalidated() {
658+
// Create frontend folder, that will otherwise be created by the first
659+
// execution, invalidating the cache on the second run
660+
testProject.newFolder("src/main/frontend")
661+
662+
val result = testProject.build("--configuration-cache", "-Pvaadin.productionMode", "vaadinBuildFrontend")
663+
result.expectTaskSucceded("vaadinBuildFrontend")
664+
assertContains(result.output, "Calculating task graph as no cached configuration is available for tasks: vaadinBuildFrontend")
665+
assertContains(result.output, "Configuration cache entry stored")
666+
667+
val buildFile = testProject.buildFile.readText()
668+
.replace("eagerServerLoad = false", "eagerServerLoad = true")
669+
testProject.buildFile.writeText(buildFile)
670+
671+
val result2 = testProject.build("--configuration-cache", "-Pvaadin.productionMode", "vaadinBuildFrontend", checkTasksSuccessful = false)
672+
result2.expectTaskOutcome("vaadinBuildFrontend", TaskOutcome.SUCCESS)
673+
assertContains(result.output, "Calculating task graph as no cached configuration is available for tasks: vaadinBuildFrontend")
674+
}
675+
676+
@Test
677+
fun testBuildFrontend_configurationCache_gradlePropertyChange_cacheInvalidated() {
678+
// Create frontend folder, that will otherwise be created by the first
679+
// execution, invalidating the cache on the second run
680+
testProject.newFolder("src/main/frontend")
681+
682+
val result = testProject.build("--configuration-cache", "-Pvaadin.productionMode", "vaadinBuildFrontend")
683+
result.expectTaskSucceded("vaadinBuildFrontend")
684+
assertContains(result.output, "Calculating task graph as no cached configuration is available for tasks: vaadinBuildFrontend")
685+
assertContains(result.output, "Configuration cache entry stored")
686+
687+
val result2 = testProject.build("--configuration-cache", "-Pvaadin.productionMode", "vaadinBuildFrontend", "-Pvaadin.eagerServerLoad=true", checkTasksSuccessful = false)
688+
result2.expectTaskOutcome("vaadinBuildFrontend", TaskOutcome.SUCCESS)
689+
assertContains(result.output, "Calculating task graph as no cached configuration is available for tasks: vaadinBuildFrontend")
690+
}
691+
692+
@Test
693+
fun testBuildFrontend_configurationCache_systemPropertyChange_cacheInvalidated() {
694+
// Create frontend folder, that will otherwise be created by the first
695+
// execution, invalidating the cache on the second run
696+
testProject.newFolder("src/main/frontend")
697+
698+
val result = testProject.build("--configuration-cache", "-Pvaadin.productionMode", "vaadinBuildFrontend")
699+
result.expectTaskSucceded("vaadinBuildFrontend")
700+
assertContains(result.output, "Calculating task graph as no cached configuration is available for tasks: vaadinBuildFrontend")
701+
assertContains(result.output, "Configuration cache entry stored")
702+
703+
val result2 = testProject.build("--configuration-cache", "-Pvaadin.productionMode", "vaadinBuildFrontend", "-Dvaadin.eagerServerLoad=true", checkTasksSuccessful = false)
704+
result2.expectTaskOutcome("vaadinBuildFrontend", TaskOutcome.SUCCESS)
705+
assertContains(result.output, "Calculating task graph as no cached configuration is available for tasks: vaadinBuildFrontend")
706+
}
707+
640708
private fun enableHilla() {
641709
testProject.newFolder(FrontendUtils.DEFAULT_FRONTEND_DIR)
642710
testProject.newFile(FrontendUtils.DEFAULT_FRONTEND_DIR + "index.ts")
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
/**
2+
* Copyright 2000-2026 Vaadin Ltd.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.vaadin.flow.gradle
17+
18+
import java.io.File
19+
import com.vaadin.experimental.FeatureFlags
20+
import com.vaadin.flow.server.Constants
21+
import org.gradle.api.provider.ListProperty
22+
import org.gradle.api.provider.Provider
23+
import org.gradle.api.tasks.Input
24+
import org.gradle.api.tasks.InputDirectory
25+
import org.gradle.api.tasks.InputFile
26+
import org.gradle.api.tasks.Optional
27+
import org.gradle.api.tasks.PathSensitive
28+
import org.gradle.api.tasks.PathSensitivity
29+
30+
/**
31+
* Declaratively defines the inputs of the [VaadinBuildFrontendTask]:
32+
* configurable parameters, frontend sources,
33+
* and package descriptor files. Used for caching the results of
34+
* vaadinBuildFrontend task to skip re-execution when inputs are unchanged.
35+
*/
36+
internal class BuildFrontendInputProperties(
37+
adapter: GradlePluginAdapter,
38+
private val toolsService: FrontendToolService
39+
) {
40+
41+
private val config = adapter.config
42+
43+
@Input
44+
fun getProductionMode(): Provider<Boolean> = config.productionMode
45+
46+
@Input
47+
@Optional
48+
fun getFrontendOutputDirectory(): Provider<String> =
49+
config.frontendOutputDirectory
50+
.absolutePath
51+
52+
@Input
53+
@Optional
54+
fun getResourcesOutputDirectory(): Provider<String> =
55+
config.resourcesOutputDirectory
56+
.absolutePath
57+
58+
@Input
59+
fun getNpmFolder(): Provider<String> = config.npmFolder.absolutePath
60+
61+
@Input
62+
fun getFrontendDirectory(): Provider<String> =
63+
config.frontendDirectory.absolutePath
64+
65+
@Input
66+
fun getGenerateBundle(): Provider<Boolean> = config.generateBundle
67+
68+
@Input
69+
fun getRunNpmInstall(): Provider<Boolean> = config.runNpmInstall
70+
71+
@Input
72+
fun getGenerateEmbeddableWebComponent(): Provider<Boolean> =
73+
config.generateEmbeddableWebComponents
74+
75+
@InputDirectory
76+
@Optional
77+
@PathSensitive(PathSensitivity.ABSOLUTE)
78+
fun getFrontendResourcesDirectory(): Provider<File> =
79+
config.frontendResourcesDirectory.filterExists()
80+
81+
@Input
82+
fun getOptimizeBundle(): Provider<Boolean> = config.optimizeBundle
83+
84+
@Input
85+
fun getEagerServerLoad(): Provider<Boolean> = config.eagerServerLoad
86+
87+
@InputFile
88+
@Optional
89+
@PathSensitive(PathSensitivity.NONE)
90+
fun getApplicationProperties(): Provider<File> =
91+
config.applicationProperties.filterExists()
92+
93+
@InputFile
94+
@Optional
95+
@PathSensitive(PathSensitivity.ABSOLUTE)
96+
fun getOpenApiJsonFile(): Provider<File> =
97+
config.openApiJsonFile.filterExists()
98+
99+
@InputFile
100+
@Optional
101+
@PathSensitive(PathSensitivity.ABSOLUTE)
102+
fun getFeatureFlagsFile(): Provider<File> = config.javaResourceFolder
103+
.map { it.resolve(FeatureFlags.PROPERTIES_FILENAME) }
104+
.filterExists()
105+
106+
@Input
107+
fun getJavaSourceFolder(): Provider<String> =
108+
config.javaSourceFolder.absolutePath
109+
110+
@Input
111+
fun getJavaResourceFolder(): Provider<String> =
112+
config.javaResourceFolder.absolutePath
113+
114+
@Input
115+
fun getGeneratedTsFolder(): Provider<String> =
116+
config.generatedTsFolder.absolutePath
117+
118+
@Input
119+
fun getPostInstallPackages(): ListProperty<String> =
120+
config.postinstallPackages
121+
122+
@Input
123+
fun getForceProductionBuild(): Provider<Boolean> =
124+
config.forceProductionBuild
125+
126+
@Input
127+
fun getReactEnable(): Provider<Boolean> = config.reactEnable
128+
129+
@Input
130+
fun getFrontendExtraFileExtensions(): ListProperty<String> =
131+
config.frontendExtraFileExtensions
132+
133+
@Input
134+
fun getNpmExcludeWebComponents(): Provider<Boolean> =
135+
config.npmExcludeWebComponents
136+
137+
@Input
138+
fun getCleanFrontendFiles(): Provider<Boolean> =
139+
config.cleanFrontendFiles
140+
141+
@Input
142+
fun getCommercialWithBanner(): Provider<Boolean> =
143+
config.commercialWithBanner
144+
145+
@InputFile
146+
@Optional
147+
@PathSensitive(PathSensitivity.NONE)
148+
fun getPackageJsonFile(): Provider<File> =
149+
config.npmFolder.map { File(it, Constants.PACKAGE_JSON) }.filterExists()
150+
151+
@InputFile
152+
@Optional
153+
@PathSensitive(PathSensitivity.NONE)
154+
fun getPackageLockJsonFile(): Provider<File> =
155+
config.npmFolder.map { File(it, Constants.PACKAGE_LOCK_JSON) }.filterExists()
156+
157+
@InputFile
158+
@Optional
159+
@PathSensitive(PathSensitivity.NONE)
160+
fun getPackageLockYamlFile(): Provider<File> =
161+
config.npmFolder.map { File(it, Constants.PACKAGE_LOCK_YAML) }.filterExists()
162+
163+
}

0 commit comments

Comments
 (0)