Skip to content

Commit

Permalink
fix: Allow Gradle builds to use custom sourcesets (#15739)
Browse files Browse the repository at this point in the history
When a sourceset other than `main` is used for holding classes using
Vaadin components, or for providing a resolution scope for Vaadin
dependencies, the `prepareVaadinFrontend` task does not correctly
extract the correct Node libraries required by those classes, and the
`buildVaadinFrontend` task subsequently fails to complete due to Vite
requiring those Node dependencies to have been extracted into the
project so failing to find all of its front-end dependencies.

The `VaadinFlowPluginExtension` is being altered to include a
`sourceSetName` field, that defaults to `main` for backwards
compatibility but allows users to specify a custom sourceset to use for
the prepare and build tasks. As the use of a custom sourceset generally
implies that different Gradle tasks will be executed for operating on
those sourcesets, and a different dependency scope generated for the
custom sourceset, fields have also been added to the extension that
allow users to override the values the plugin uses, whilst the default
values follow the Gradle conventions for both `main` and non-`main`
sourcesets.

Fixes #15738
  • Loading branch information
mc1arke committed Jan 25, 2023
1 parent 7286880 commit 00bdfdf
Show file tree
Hide file tree
Showing 6 changed files with 157 additions and 31 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* Copyright 2000-2022 Vaadin Ltd
* Copyright 2000-2023 Vaadin Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -532,4 +532,97 @@ class MiscSingleModuleTest : AbstractGradleTest() {
classpath.map { it.removeSuffix("-SNAPSHOT.jar").dropLastWhile { it != '-' } }
}
}

@Test
fun testUsingNonMainSourceSet() {
testProject.settingsFile.writeText(
"""
pluginManagement {
repositories {
gradlePluginPortal()
}
}
"""
)
testProject.buildFile.writeText(
"""
plugins {
id 'java'
id 'org.springframework.boot' version '3.0.0'
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
id("com.vaadin")
}
repositories {
mavenLocal()
mavenCentral()
maven { url 'https://maven.vaadin.com/vaadin-prereleases' }
}
sourceSets {
ui {
java
}
main {
java {
compileClasspath += ui.output
runtimeClasspath += ui.output + ui.runtimeClasspath
}
}
}
vaadin {
productionMode = true
sourceSetName = 'ui'
}
dependencies {
uiImplementation('com.vaadin:flow:$flowVersion')
implementation('org.springframework.boot:spring-boot-starter-web')
}
jar {
enabled = false
}
"""
)

testProject.newFile(
"src/main/java/com/example/demo/DemoApplication.java", """
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
""".trimIndent()
)

testProject.newFile(
"src/ui/java/com/example/demo/AppShell.java", """
package com.example.demo;
import com.vaadin.flow.component.page.AppShellConfigurator;
import com.vaadin.flow.server.PWA;
@PWA(name = "Demo application", shortName = "Demo")
public class AppShell implements AppShellConfigurator {
}
""".trimIndent()
)

val build: BuildResult = testProject.build("build")
build.expectTaskSucceded("vaadinPrepareFrontend")
build.expectTaskSucceded("vaadinBuildFrontend")

val jar: File = testProject.builtJar
expectArchiveContainsVaadinBundle(jar, true)
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2000-2022 Vaadin Ltd
* Copyright 2000-2023 Vaadin Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -45,10 +45,10 @@ internal class GradlePluginAdapter(val project: Project, private val isBeforePro
override fun generatedTsFolder(): File = extension.generatedTsFolder

override fun getClassFinder(): ClassFinder {
val runtimeClasspath: Configuration? = project.configurations.findByName("runtimeClasspath")
val runtimeClasspathJars: List<File> = if (runtimeClasspath != null) {
val dependencyConfiguration: Configuration? = project.configurations.findByName(extension.dependencyScope!!)
val dependencyConfigurationJars: List<File> = if (dependencyConfiguration != null) {
var artifacts: List<ResolvedArtifact> =
runtimeClasspath.resolvedConfiguration.resolvedArtifacts.toList()
dependencyConfiguration.resolvedConfiguration.resolvedArtifacts.toList()
val extension = VaadinFlowPluginExtension.get(project)
val artifactFilter = extension.classpathFilter.toPredicate()
artifacts = artifacts.filter { artifactFilter.test(it.moduleVersion.id.module) }
Expand All @@ -57,11 +57,11 @@ internal class GradlePluginAdapter(val project: Project, private val isBeforePro

// we need to also analyze the project's classes
val sourceSet: SourceSetContainer = project.properties["sourceSets"] as SourceSetContainer
val classesDirs: List<File> = sourceSet.getByName("main").output.classesDirs
val classesDirs: List<File> = sourceSet.getByName(extension.sourceSetName).output.classesDirs
.toList()
.filter { it.exists() }

val resourcesDir: List<File> = listOfNotNull(sourceSet.getByName("main").output.resourcesDir)
val resourcesDir: List<File> = listOfNotNull(sourceSet.getByName(extension.sourceSetName).output.resourcesDir)
.filter { it.exists() }

// for Spring Boot project there is no "providedCompile" scope: the WAR plugin brings that in.
Expand All @@ -71,7 +71,7 @@ internal class GradlePluginAdapter(val project: Project, private val isBeforePro
?.toList()
?: listOf()

val apis: Set<File> = (runtimeClasspathJars + classesDirs + resourcesDir + servletJar).toSet()
val apis: Set<File> = (dependencyConfigurationJars + classesDirs + resourcesDir + servletJar).toSet()

// eagerly check that all the files/folders exist, to avoid spamming the console later on
// see https://github.com/vaadin/vaadin-gradle-plugin/issues/38 for more details
Expand All @@ -94,7 +94,7 @@ internal class GradlePluginAdapter(val project: Project, private val isBeforePro
}

override fun getJarFiles(): MutableSet<File> {
val jarFiles: Set<File> = project.configurations.runtimeClasspath.jars.toSet()
val jarFiles: Set<File> = project.configurations.getByName(extension.dependencyScope!!).jars.toSet()
return jarFiles.toMutableSet()
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* Copyright 2000-2022 Vaadin Ltd
* Copyright 2000-2023 Vaadin Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -18,7 +18,6 @@ package com.vaadin.gradle
import com.vaadin.flow.server.Constants
import com.vaadin.flow.server.InitParameters
import com.vaadin.flow.server.frontend.FrontendTools
import com.vaadin.flow.server.frontend.FrontendUtils
import com.vaadin.flow.server.frontend.installer.NodeInstaller
import groovy.lang.Closure
import groovy.lang.DelegatesTo
Expand Down Expand Up @@ -190,6 +189,27 @@ public open class VaadinFlowPluginExtension(project: Project) {

public var classpathFilter: ClasspathFilter = ClasspathFilter()

/**
* The name of the SourceSet to scan for Vaadin components - i.e. the classes that are annoated with
* Vaadin annotations.
*/
public var sourceSetName : String = "main"

/**
* The Gradle scope the Vaadin dependencies have been added to. Defaults to 'runtimeClasspath' if
* no sourceSetName has been specified, or '<code>sourceSetName</code>RuntimeClasspath' if a non-main sourceset
* has been set.
*/
public var dependencyScope : String? = null

/**
* The Gradle task that the `vaadinPrepareFrontend` task must run before. The target task should run before
* or be the task that copies the files from the resources directories of the specified SourceSet to the relevant
* output directory for that SourceSet. Defaults to 'processResources' if no sourceSetName has been specified, or
* 'process<code>SourceSetName</code>Resources' if a non-main sourceset has been specified.
*/
public var processResourcesTaskName : String? = null

public fun filterClasspath(@DelegatesTo(value = ClasspathFilter::class, strategy = Closure.DELEGATE_FIRST) block: Closure<*>? = null): ClasspathFilter {
if (block != null) {
block.delegate = classpathFilter
Expand Down Expand Up @@ -234,6 +254,24 @@ public open class VaadinFlowPluginExtension(project: Project) {
if (useGlobalPnpmProperty != null) {
useGlobalPnpm = useGlobalPnpmProperty
}

// calculate processResourcesTaskName if not set by user
if (processResourcesTaskName == null) {
processResourcesTaskName = if (sourceSetName == "main") {
"processResources"
} else {
"process${sourceSetName.replaceFirstChar(Char::titlecase)}Resources"
}
}

// calculate dependencyScope if not set by user
if (dependencyScope == null) {
dependencyScope = if (sourceSetName == "main") {
"runtimeClasspath"
} else {
sourceSetName + "RuntimeClasspath"
}
}
}

override fun toString(): String = "VaadinFlowPluginExtension(" +
Expand All @@ -258,13 +296,16 @@ public open class VaadinFlowPluginExtension(project: Project) {
"generatedTsFolder=$generatedTsFolder, " +
"nodeVersion=$nodeVersion, " +
"nodeDownloadRoot=$nodeDownloadRoot, " +
"nodeAutoUpdate=$nodeAutoUpdate" +
"resourceOutputDirectory=$resourceOutputDirectory" +
"postinstallPackages=$postinstallPackages" +
"nodeAutoUpdate=$nodeAutoUpdate, " +
"resourceOutputDirectory=$resourceOutputDirectory, " +
"postinstallPackages=$postinstallPackages, " +
"sourceSetName=$sourceSetName, " +
"dependencyScope=$dependencyScope, " +
"processResourcesTaskName=$processResourcesTaskName" +
")"
}

internal val Project.buildResourcesDir: File get() {
val sourceSets: SourceSetContainer = project.convention.getPlugin(JavaPluginConvention::class.java).sourceSets
return sourceSets.getByName("main").output.resourcesDir!!
return sourceSets.getByName(extensions.getByType(VaadinFlowPluginExtension::class.java).sourceSetName).output.resourcesDir!!
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* Copyright 2000-2022 Vaadin Ltd
* Copyright 2000-2023 Vaadin Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -49,10 +49,10 @@ public class VaadinPlugin : Plugin<Project> {

// add a new source-set folder for generated stuff, by default `vaadin-generated`
val sourceSets: SourceSetContainer = it.properties["sourceSets"] as SourceSetContainer
sourceSets.getByName("main").resources.srcDirs(extension.resourceOutputDirectory)
sourceSets.getByName(extension.sourceSetName).resources.srcDirs(extension.resourceOutputDirectory)

// auto-activate tasks: https://github.com/vaadin/vaadin-gradle-plugin/issues/48
project.tasks.getByPath("processResources").dependsOn("vaadinPrepareFrontend")
project.tasks.getByPath(extension.processResourcesTaskName!!).dependsOn("vaadinPrepareFrontend")
if (extension.productionMode) {
// this will also catch the War task since it extends from Jar
project.tasks.withType(Jar::class.java) { task: Jar ->
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* Copyright 2000-2022 Vaadin Ltd
* Copyright 2000-2023 Vaadin Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -18,8 +18,6 @@ package com.vaadin.gradle
import com.vaadin.flow.plugin.base.BuildFrontendUtil
import com.vaadin.flow.server.frontend.FrontendTools
import org.gradle.api.DefaultTask
import org.gradle.api.Task
import org.gradle.api.artifacts.ProjectDependency
import org.gradle.api.tasks.TaskAction

/**
Expand All @@ -33,17 +31,18 @@ public open class VaadinPrepareFrontendTask : DefaultTask() {
group = "Vaadin"
description = "checks that node and npm tools are installed, copies frontend resources available inside `.jar` dependencies to `node_modules`, and creates or updates `package.json` and `webpack.config.json` files."

val extension: VaadinFlowPluginExtension = VaadinFlowPluginExtension.get(project)
// Maven's task run in the LifecyclePhase.PROCESS_RESOURCES phase

// the processResources copies stuff from build/vaadin-generated
// (which is populated by this task) and therefore must run after this task.
project.tasks.getByName("processResources").mustRunAfter("vaadinPrepareFrontend")
project.tasks.getByName(extension.processResourcesTaskName!!).mustRunAfter("vaadinPrepareFrontend")

// make sure all dependent projects have finished building their jars, otherwise
// the Vaadin classpath scanning will not work properly. See
// https://github.com/vaadin/vaadin-gradle-plugin/issues/38
// for more details.
dependsOn(project.configurations.runtimeClasspath.jars)
dependsOn(project.configurations.getByName(extension.dependencyScope!!).jars)
}

@TaskAction
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* Copyright 2000-2022 Vaadin Ltd
* Copyright 2000-2023 Vaadin Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -17,7 +17,6 @@ package com.vaadin.gradle


import org.gradle.api.artifacts.Configuration
import org.gradle.api.artifacts.ConfigurationContainer
import org.gradle.api.file.FileCollection
import com.vaadin.flow.function.SerializableSupplier
import com.vaadin.flow.server.frontend.FrontendTools
Expand Down Expand Up @@ -74,12 +73,6 @@ internal fun VaadinFlowPluginExtension.createFrontendTools(): FrontendTools {
return FrontendTools(settings)
}

/**
* Returns the "runtimeClasspath" file collection.
*/
internal val ConfigurationContainer.runtimeClasspath: Configuration
get() = getByName("runtimeClasspath")

/**
* Returns only jar files from given file collection.
*/
Expand Down

0 comments on commit 00bdfdf

Please sign in to comment.