GradleVaadin/src/dorkbox/gradleVaadin/Vaadin.kt

369 lines
15 KiB
Kotlin

/*
* Copyright 2020 dorkbox, llc
*
* 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 dorkbox.gradleVaadin
import dorkbox.gradleVaadin.node.deps.DependencyScanner
import dorkbox.gradleVaadin.node.npm.proxy.ProxySettings
import dorkbox.gradleVaadin.node.npm.task.NpmInstallTask
import dorkbox.gradleVaadin.node.npm.task.NpmSetupTask
import dorkbox.gradleVaadin.node.npm.task.NpmTask
import dorkbox.gradleVaadin.node.npm.task.NpxTask
import dorkbox.gradleVaadin.node.task.NodeSetupTask
import dorkbox.gradleVaadin.node.task.NodeTask
import dorkbox.vaadin.VaadinApplication
import org.gradle.api.Action
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.Task
import org.gradle.api.execution.TaskExecutionGraph
import org.gradle.api.tasks.TaskInputs
import org.gradle.jvm.tasks.Jar
/**
* For managing Vaadin gradle tasks
*
* NOTE: Vaadin css resources are compiled into the stats.json file, they are NOT loaded "statically" from the webserver
*/
@Suppress("UnstableApiUsage", "unused", "SameParameterValue", "ObjectLiteralToLambda")
class Vaadin : Plugin<Project> {
companion object {
internal const val NODE_GROUP = "Node"
internal const val NPM_GROUP = "npm"
internal const val SHUTDOWN_TASK = "shutdownCompiler"
internal const val compileDevName = "vaadinDevelopment"
internal const val compileProdName = "vaadinProduction"
/**
* Recursively resolves all child dependencies of the project
*
* THIS MUST BE IN "afterEvaluate" or run from a specific task.
*/
fun resolveAllDependencies(project: Project): List<DependencyScanner.DependencyData> {
// NOTE: we cannot createTree("compile") and createTree("runtime") using the same exitingNames and expect correct results.
// This is because a dependency might exist for compile and runtime, but have different children, therefore, the list
// will be incomplete
// there will be DUPLICATES! (we don't care about children or hierarchy, so we remove the dupes)
return (DependencyScanner.scan(project, "compileClasspath") +
DependencyScanner.scan(project, "runtimeClasspath")
).toSet().toList()
}
/**
* Recursively resolves all child compile dependencies of the project
*
* THIS MUST BE IN "afterEvaluate" or run from a specific task.
*/
fun resolveRuntimeDependencies(project: Project): DependencyScanner.ProjectDependencies {
val projectDependencies = mutableListOf<DependencyScanner.Dependency>()
val existingNames = mutableMapOf<String, DependencyScanner.Dependency>()
DependencyScanner.createTree(project, "runtimeClasspath", projectDependencies, existingNames)
return DependencyScanner.ProjectDependencies(projectDependencies, existingNames.map { it.value })
}
}
private lateinit var project: Project
private lateinit var config: VaadinConfig
/** Useful, so we can announce the version of vaadin we are using */
val version = VaadinApplication.vaadinVersion
private fun newTask(dependencyTask: Task,
taskName: String,
description: String,
inputs: TaskInputs.()->Unit = {},
action: Task.(vaadinCompiler: VaadinCompiler) -> Unit): Task {
return newTask(dependencyTask.name, taskName, description, inputs, action)
}
@Suppress("ObjectLiteralToLambda")
private fun newTask(dependencyName: String,
taskName: String,
description: String,
inputs: TaskInputs.()->Unit = {},
action: Task.(vaadinCompiler: VaadinCompiler) -> Unit): Task {
return newTask(listOf(dependencyName), taskName, description, inputs, action)
}
private fun newTask(dependencyName: List<String>,
taskName: String,
description: String,
inputs: TaskInputs.()->Unit = {},
action: Task.(vaadinCompiler: VaadinCompiler) -> Unit): Task {
return project.tasks.create(taskName).apply {
dependsOn(dependencyName.toTypedArray())
finalizedBy(SHUTDOWN_TASK)
group = "vaadin"
this.description = description
inputs(this.inputs)
doLast(object: Action<Task> {
override fun execute(task: Task) {
action(task, VaadinConfig[project].vaadinCompiler)
}
})
}
}
override fun apply(project: Project) {
this.project = project
// https://discuss.gradle.org/t/can-a-plugin-itself-add-buildscript-dependencies-and-then-apply-a-plugin/25039/4
apply("java")
// Create the Plugin extension object (for users to configure publishing).
config = VaadinConfig.create(project)
project.repositories.apply {
maven { it.setUrl("https://maven.vaadin.com/vaadin-addons") } // Vaadin Addons
maven { it.setUrl("https://maven.vaadin.com/vaadin-prereleases") } // Pre-releases
maven { it.setUrl("https://oss.sonatype.org/content/repositories/vaadin-snapshots") } // Vaadin Snapshots
}
project.dependencies.apply {
add("implementation", "com.vaadin:vaadin:${VaadinConfig.VAADIN_VERSION}")
add("implementation", "com.dorkbox:VaadinUndertow:${VaadinConfig.MAVEN_VAADIN_GRADLE_VERSION}")
add("implementation", "io.undertow:undertow-core:${VaadinConfig.UNDERTOW_VERSION}")
add("implementation", "io.undertow:undertow-servlet:${VaadinConfig.UNDERTOW_VERSION}")
add("implementation", "io.undertow:undertow-websockets-jsr:${VaadinConfig.UNDERTOW_VERSION}")
}
// NOTE: NPM will ALWAYS install packages to the "node_modules" directory that is a sibling to the packages.json directory!
addGlobalTypes()
addTasks()
addNpmRule()
project.gradle.taskGraph.whenReady(object: Action<TaskExecutionGraph> {
override fun execute(graph: TaskExecutionGraph) {
// every other task will do nothing (run as dev mode).
val allTasks = graph.allTasks
var hasVaadinTask = false
if (allTasks.firstOrNull { it.name == compileProdName } != null) {
config.productionMode.set(true)
hasVaadinTask = true
}
if (allTasks.firstOrNull { it.name == compileDevName } != null) {
hasVaadinTask = true
}
if (hasVaadinTask) {
val jarTasks = allTasks.filter { it.name.endsWith("jar")}
if (jarTasks.isNotEmpty()) {
project.tasks.withType(Jar::class.java) {
// we ALWAYS want to make sure that this task runs. If *something* is cached, then there jar file output will be incomplete.
it.outputs.cacheIf { false }
it.outputs.upToDateWhen { false }
}
}
}
}
})
project.tasks.create(SHUTDOWN_TASK).apply {
doLast(object: Action<Task> {
override fun execute(t: Task) {
val vaadinCompiler = VaadinConfig[project].vaadinCompiler
// every task MUST call shutdown in order to close the class-scanner (otherwise it will hold open files)
vaadinCompiler.finish()
}
})
}
// NOTE! our class-scanner scans COMPILED CLASSES, so it is required to depend (at some point) on class compilation!
val generateWebComponents = newTask(listOf(NodeSetupTask.NAME, "classes"), "generateWebComponents", "Generate Vaadin web components")
{ vaadinCompiler ->
VaadinConfig[project].vaadinCompiler.log()
vaadinCompiler.generateWebComponents()
}
val createMissingPackageJson = newTask(generateWebComponents, "createMissingPackageJson", "Prepare Vaadin frontend")
{ vaadinCompiler ->
// VaadinCompile.print()
vaadinCompiler.createMissingPackageJson()
}
val prepareJsonFiles = newTask(createMissingPackageJson, "prepareJsonFiles", "Prepare Vaadin frontend",
{
// files(vaadinCompiler.jsonPackageFile)
})
{ vaadinCompiler ->
vaadinCompiler.prepareJsonFiles()
}
val copyJarResources = newTask(prepareJsonFiles, "copyJarResources", "Compile Vaadin resources for Production")
{ vaadinCompiler ->
vaadinCompiler.copyJarResources()
}
val copyLocalResources = newTask(copyJarResources, "copyLocalResources", "Compile Vaadin resources for Production")
{ vaadinCompiler ->
vaadinCompiler.copyFrontendResourcesDirectory()
}
val createTokenFile = newTask(copyLocalResources, "createTokenFile", "Create Vaadin token file")
{ vaadinCompiler ->
vaadinCompiler.createTokenFile()
}
val updateWebPack = newTask(createTokenFile, "updateWebPack", "Compile Vaadin resources for Production")
{ vaadinCompiler ->
vaadinCompiler.fixWebpackTemplate()
}
// last one from NodeTasks.java
val enableImportsUpdate = newTask(updateWebPack, "enableImportsUpdate", "Compile Vaadin resources for Production")
{ vaadinCompiler ->
vaadinCompiler.enableImportsUpdate()
}
// FOR DEV MODE, THIS IS AS FAR AS WE GO
// last DevModeHandler start
// the plugin on start needs to rewrite DevModeInitializer.initDevModeHandler so that all these tasks aren't run every time for dev mode
// because that is really slow to start up??
// ALTERNATIVELY, devmodeinit runs the same thing as the gradle plugin, but the difference is we only run the full gradle version
// for a production build (webpack is compiled instead of running dev-mode, is the only difference...).
// the only problem, is when running as a JPMS module...
// to solve this, we could modify the byte-code -- then RECREATE the jar (which we would then startup)
// OR.. we modify the bytecode ON COMPILE, so that a "run" would always include the modified jar
// or we just include our own version
val generateWebPack = newTask(enableImportsUpdate, "generateWebPack", "Compile Vaadin resources for Production")
{ vaadinCompiler ->
vaadinCompiler.generateWebPack()
}
project.tasks.create(compileDevName).apply {
dependsOn(createTokenFile)
finalizedBy(SHUTDOWN_TASK)
group = "vaadin"
description = "Compile Vaadin resources for Development"
outputs.cacheIf { false }
outputs.upToDateWhen { false }
inputs.files(
"${project.projectDir}/package.json",
"${project.projectDir}/package-lock.json",
"${project.projectDir}/webpack.config.js",
"${project.projectDir}/webpack.production.js"
)
outputs.dir("${project.buildDir}/config")
outputs.dir("${project.buildDir}/resources")
outputs.dir("${project.buildDir}/nodejs")
outputs.dir("${project.buildDir}/node_modules")
}
project.tasks.create(compileProdName).apply {
dependsOn(generateWebPack)
finalizedBy(SHUTDOWN_TASK)
group = "vaadin"
description = "Compile Vaadin resources for Production"
outputs.cacheIf { false }
outputs.upToDateWhen { false }
// inputs.files(
// "${project.projectDir}/package.json",
// "${project.projectDir}/package-lock.json",
// "${project.projectDir}/webpack.config.js",
// "${project.projectDir}/webpack.production.js"
// )
//
// outputs.dir("${project.buildDir}/resources/main/META-INF/resources/VAADIN")
// outputs.dir("${project.buildDir}/node_modules")
}
project.tasks.withType(Jar::class.java) {
// we ALWAYS want to make sure that this task runs when jar files are created (since we consider a jar to be the final production package for this project
it.dependsOn(compileProdName)
}
// config.addSubprojects()
// project.childProjects.values.forEach {
// it.pluginManager.apply(Vaadin::class.java)
// }
}
// required to make sure the plugins are correctly applied. ONLY applying it to the project WILL NOT work.
// The plugin must also be applied to the root project
private fun apply(id: String) {
if (project.rootProject.pluginManager.findPlugin(id) == null) {
project.rootProject.pluginManager.apply(id)
}
if (project.pluginManager.findPlugin(id) == null) {
project.pluginManager.apply(id)
}
}
private fun addGlobalTypes() {
addGlobalType<NodeTask>()
addGlobalType<NpmTask>()
addGlobalType<NpxTask>()
addGlobalType<ProxySettings>()
}
private inline fun <reified T> addGlobalType() {
project.extensions.extraProperties[T::class.java.simpleName] = T::class.java
}
private fun addTasks() {
project.tasks.register(NpmInstallTask.NAME, NpmInstallTask::class.java)
project.tasks.register(NodeSetupTask.NAME, NodeSetupTask::class.java)
project.tasks.register(NpmSetupTask.NAME, NpmSetupTask::class.java)
}
private fun addNpmRule() { // note this rule also makes it possible to specify e.g. "dependsOn npm_install"
project.tasks.addRule("Pattern: \"npm_<command>\": Executes an NPM command.") { taskName ->
if (taskName.startsWith("npm_")) {
project.tasks.create(taskName, NpmTask::class.java) {
val tokens = taskName.split("_").drop(1) // all except first
it.npmCommand.set(tokens)
if (tokens.first().equals("run", ignoreCase = true)) {
it.dependsOn(NpmInstallTask.NAME)
}
}
}
}
}
}