From d840c1d895b9001de192c80c6ed961ffaeffa598 Mon Sep 17 00:00:00 2001 From: Ashar Fuadi Date: Sun, 8 Mar 2020 21:26:03 +0700 Subject: [PATCH] Implement support for WebJars and LESS assets pipeline --- build.gradle.kts | 14 ++ .../gradle/playframework/UserGuidePlugin.kt | 2 + src/docs/asciidoc/13-tasks.adoc | 4 + src/docs/asciidoc/14-source-sets.adoc | 13 +- src/docs/asciidoc/20-usage.adoc | 4 +- src/docs/asciidoc/26-asset-pipelines.adoc | 67 +++++++ .../asciidoc/40-migrating-software-model.adoc | 1 - .../samples/source-sets/groovy/build.gradle | 4 + ...layBinaryAdvancedAppIntegrationTest.groovy | 14 +- .../PlayLessPluginIntegrationTest.groovy | 77 +++++++ ...essWithWebJarsPluginIntegrationTest.groovy | 53 +++++ .../PlayWebJarsPluginIntegrationTest.groovy | 40 ++++ .../AbstractAssetsTaskIntegrationTest.groovy | 34 ++++ ...ractJavaScriptMinifyIntegrationTest.groovy | 25 +-- .../JavaScriptMinifyIntegrationTest.groovy | 4 - .../tasks/LessCompileIntegrationTest.groovy | 188 ++++++++++++++++++ .../WebJarsExtractIntegrationTest.groovy | 77 +++++++ .../app/assets/stylesheets/extra.less | 3 + .../advancedplayapp/app/views/main.scala.html | 1 + .../fixtures/app/advancedplayapp/build.gradle | 4 + .../plugins/PlayApplicationPlugin.java | 3 +- .../plugins/PlayJavaScriptPlugin.java | 9 +- .../playframework/plugins/PlayLessPlugin.java | 72 +++++++ .../plugins/PlayWebJarsPlugin.java | 81 ++++++++ .../sourcesets/LessSourceSet.java | 38 ++++ .../internal/DefaultLessSourceSet.java | 32 +++ .../playframework/tasks/LessCompile.java | 90 +++++++++ .../playframework/tasks/WebJarsExtract.java | 60 ++++++ .../tasks/internal/LessCompileRunnable.java | 25 +++ .../internal/WebJarsExtractRunnable.java | 22 ++ .../internal/less/DefaultLessCompileSpec.java | 33 +++ .../tools/internal/less/Less4jCompiler.java | 108 ++++++++++ .../tools/internal/less/LessCompileSpec.java | 14 ++ .../webjars/DefaultWebJarsExtractSpec.java | 24 +++ .../internal/webjars/WebJarsExtractSpec.java | 11 + .../internal/webjars/WebJarsExtractor.java | 62 ++++++ 36 files changed, 1270 insertions(+), 43 deletions(-) create mode 100644 src/docs/asciidoc/26-asset-pipelines.adoc create mode 100644 src/integTest/groovy/org/gradle/playframework/plugins/PlayLessPluginIntegrationTest.groovy create mode 100644 src/integTest/groovy/org/gradle/playframework/plugins/PlayLessWithWebJarsPluginIntegrationTest.groovy create mode 100644 src/integTest/groovy/org/gradle/playframework/plugins/PlayWebJarsPluginIntegrationTest.groovy create mode 100644 src/integTest/groovy/org/gradle/playframework/tasks/AbstractAssetsTaskIntegrationTest.groovy create mode 100644 src/integTest/groovy/org/gradle/playframework/tasks/LessCompileIntegrationTest.groovy create mode 100644 src/integTest/groovy/org/gradle/playframework/tasks/WebJarsExtractIntegrationTest.groovy create mode 100644 src/integTestFixtures/resources/org/gradle/playframework/fixtures/app/advancedplayapp/app/assets/stylesheets/extra.less create mode 100644 src/main/java/org/gradle/playframework/plugins/PlayLessPlugin.java create mode 100644 src/main/java/org/gradle/playframework/plugins/PlayWebJarsPlugin.java create mode 100644 src/main/java/org/gradle/playframework/sourcesets/LessSourceSet.java create mode 100644 src/main/java/org/gradle/playframework/sourcesets/internal/DefaultLessSourceSet.java create mode 100644 src/main/java/org/gradle/playframework/tasks/LessCompile.java create mode 100644 src/main/java/org/gradle/playframework/tasks/WebJarsExtract.java create mode 100644 src/main/java/org/gradle/playframework/tasks/internal/LessCompileRunnable.java create mode 100644 src/main/java/org/gradle/playframework/tasks/internal/WebJarsExtractRunnable.java create mode 100644 src/main/java/org/gradle/playframework/tools/internal/less/DefaultLessCompileSpec.java create mode 100644 src/main/java/org/gradle/playframework/tools/internal/less/Less4jCompiler.java create mode 100644 src/main/java/org/gradle/playframework/tools/internal/less/LessCompileSpec.java create mode 100644 src/main/java/org/gradle/playframework/tools/internal/webjars/DefaultWebJarsExtractSpec.java create mode 100644 src/main/java/org/gradle/playframework/tools/internal/webjars/WebJarsExtractSpec.java create mode 100644 src/main/java/org/gradle/playframework/tools/internal/webjars/WebJarsExtractor.java diff --git a/build.gradle.kts b/build.gradle.kts index 1d7439c2..5f081320 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -53,6 +53,20 @@ gradlePlugin { implementationClass = "org.gradle.playframework.plugins.PlayRoutesPlugin" } + register("play-less-plugin") { + id = "org.gradle.playframework-less" + displayName = "Play LESS Plugin" + description = "Plugin for compiling LESS stylesheets in a Play application." + implementationClass = "org.gradle.playframework.plugins.PlayLessPlugin" + } + + register("play-webjars-plugin") { + id = "org.gradle.playframework-webjars" + displayName = "Play WebJars Plugin" + description = "Plugin for extracting WebJars in a Play application." + implementationClass = "org.gradle.playframework.plugins.PlayWebJarsPlugin" + } + register("play-application-plugin") { id = "org.gradle.playframework-application" displayName = "Play Application Plugin" diff --git a/buildSrc/src/main/kotlin/org/gradle/playframework/UserGuidePlugin.kt b/buildSrc/src/main/kotlin/org/gradle/playframework/UserGuidePlugin.kt index 6c90ee32..7bb5c827 100644 --- a/buildSrc/src/main/kotlin/org/gradle/playframework/UserGuidePlugin.kt +++ b/buildSrc/src/main/kotlin/org/gradle/playframework/UserGuidePlugin.kt @@ -57,6 +57,8 @@ class UserGuidePlugin : Plugin { val htmlUserGuideFile = file("$outputDir/html5/index.html") var text = htmlUserGuideFile.readText() text = text.replace(Regex("id 'org.gradle.playframework' version '.+'"), "id 'org.gradle.playframework' version '${project.version}'") + text = text.replace(Regex("id 'org.gradle.playframework-less' version '.+'"), "id 'org.gradle.playframework-less' version '${project.version}'") + text = text.replace(Regex("id 'org.gradle.playframework-webjars' version '.+'"), "id 'org.gradle.playframework-webjars' version '${project.version}'") htmlUserGuideFile.writeText(text) } } diff --git a/src/docs/asciidoc/13-tasks.adoc b/src/docs/asciidoc/13-tasks.adoc index 947461c1..c083a17e 100644 --- a/src/docs/asciidoc/13-tasks.adoc +++ b/src/docs/asciidoc/13-tasks.adoc @@ -14,6 +14,10 @@ _Depends on_: `stageMainDist` + Stages the Play distribution. +`createPlayAssetsJar` — {uri-gradle-dsl-reference}/org.gradle.api.Task.html[Task]:: ++ +Bundles asset files into a jar file. + ==== Running and testing tasks The plugin also provides tasks for running, testing and packaging your Play application. diff --git a/src/docs/asciidoc/14-source-sets.adoc b/src/docs/asciidoc/14-source-sets.adoc index 11a3758a..9883e034 100644 --- a/src/docs/asciidoc/14-source-sets.adoc +++ b/src/docs/asciidoc/14-source-sets.adoc @@ -5,32 +5,43 @@ One type of element that describes the application are the source sets that defi .Default Play source sets [%header%autowidth,compact] |=== -| Source Set | Type | Directory | Filters +| Source Set | Type | Directory | Filters | Plugin extension | java | {uri-gradle-dsl-reference}/org.gradle.api.tasks.SourceSet.html[SourceSet] | app | \**/*.java +| (built-in) | scala | {uri-gradle-dsl-reference}/org.gradle.api.tasks.SourceSet.html[SourceSet] | app | \**/*.scala +| (built-in) | routes | link:{uri-plugin-api}/org/gradle/playframework/sourcesets/RoutesSourceSet.html[RoutesSourceSet] | conf | routes, *.routes +| (built-in) | twirl | link:{uri-plugin-api}/org/gradle/playframework/sourcesets/TwirlSourceSet.html[TwirlSourceSet] | app | \**/*.scala.* +| (built-in) | javaScript | link:{uri-plugin-api}/org/gradle/playframework/sourcesets/JavaScriptSourceSet.html[JavaScriptSourceSet] | app/assets | \**/*.js +| (built-in) + +| less +| link:{uri-plugin-api}/org/gradle/playframework/sourcesets/LessSourceSet.html[LessSourceSet] +| app/assets +| \**/*.less +| org.gradle.playframework-less |=== These <>. \ No newline at end of file diff --git a/src/docs/asciidoc/20-usage.adoc b/src/docs/asciidoc/20-usage.adoc index bb2f203c..0a831d10 100644 --- a/src/docs/asciidoc/20-usage.adoc +++ b/src/docs/asciidoc/20-usage.adoc @@ -8,4 +8,6 @@ include::23-package-distribution.adoc[] include::24-multi-project.adoc[] -include::25-ide.adoc[] \ No newline at end of file +include::25-ide.adoc[] + +include::26-asset-pipelines.adoc[] diff --git a/src/docs/asciidoc/26-asset-pipelines.adoc b/src/docs/asciidoc/26-asset-pipelines.adoc new file mode 100644 index 00000000..e3f5cbdd --- /dev/null +++ b/src/docs/asciidoc/26-asset-pipelines.adoc @@ -0,0 +1,67 @@ +=== Adding asset pipelines + +The Play plugin's `createPlayAssetsJar` task (on which `runPlay` and `dist` tasks depend) bundles all assets into a single jar file. This jar file is included in the distribution package to be served by the Play application. + +The following asset pipelines are supported. + +==== Public assets + +Files in the `public/` directory will be included in the assets JAR without any processing. + +==== JavaScript minification + +Files in the `javaScript` source set (the default one is `app/assets` with `\**/*.js` filter), will be minified using Google Closure compiler and then included in the assets jar. + +==== WebJars + +WebJar libraries will be extracted and then included in the assets jar, under the `lib/` subdirectory. + +To apply this pipeline, apply the `org.gradle.playframework-webjars` plugin as well: + +[source,groovy] +.build.gradle +---- +plugins { + id 'org.gradle.playframework' version '0.9' + id 'org.gradle.playframework-webjars' version '0.9' +} +---- + +To add a WebJar library, add it to the dependency list using `webJar` configuration, e.g.: + +[source,groovy] +.build.gradle +---- +dependencies { + webJar 'org.webjars:requirejs:2.3.6' +} +---- + +==== LESS compilation + +Files in the `less` source set (the default one is `app/assets` with `\**/*.less` filter) will be compiled into CSS and then included in the assets jar. + +To apply this pipeline, apply the `org.gradle.playframework-less` plugin as well: + +[source,groovy] +.build.gradle +---- +plugins { + id 'org.gradle.playframework' version '0.9' + id 'org.gradle.playframework-less' version '0.9' +} +---- + +The following `\@import`s in LESS files are supported: + +- Partial LESS files (ones with start with underscores, such ass `_common.less`). +- WebJar extracted files (using the WebJar pipeline described above), with `lib/` subdirectory prefix. + +Example: + +[source,less] +.main.css +---- +@import "./_common.less"; +@import "lib/css-reset/reset.css"; +---- \ No newline at end of file diff --git a/src/docs/asciidoc/40-migrating-software-model.adoc b/src/docs/asciidoc/40-migrating-software-model.adoc index 02815a8a..86f4f1ea 100644 --- a/src/docs/asciidoc/40-migrating-software-model.adoc +++ b/src/docs/asciidoc/40-migrating-software-model.adoc @@ -7,6 +7,5 @@ The following features are not available in this plugin: * The custom configurations `playTest` and `playRun` do not exist anymore. Use the standard configurations `implementation`, `testImplementation` and `runtime` of the Java/Scala plugin instead. * The extension does not allow for configuring a target platform. You will need to configure Play, Scala and Java version individually. * {uri-gradle-userguide}//play_plugin.html#sec:adding_extra_source_sets[Adding new source sets] of a specific type is a built-in feature of the software model. This functionality is currently not available. -* The concept of an "asset" does not exist and therefore cannot be used to {uri-gradle-userguide}/play_plugin.html#sec:injecting_a_custom_asset_pipeline[configure a custom asset pipeline]. * Source sets cannot be added by type. You will need to add additional source directories to the existing source sets provided by the plugin. * The CoffeeScript plugin is not available anymore. \ No newline at end of file diff --git a/src/docs/samples/source-sets/groovy/build.gradle b/src/docs/samples/source-sets/groovy/build.gradle index 70ed8c32..85252dd0 100644 --- a/src/docs/samples/source-sets/groovy/build.gradle +++ b/src/docs/samples/source-sets/groovy/build.gradle @@ -30,6 +30,10 @@ sourceSets { srcDir 'additional/javascript' exclude '**/old_*.js' } + less { + srcDir 'additional/less' + exclude '**/old_*.less' + } } } // end::add-source-directories[] diff --git a/src/integTest/groovy/org/gradle/playframework/application/advanced/PlayBinaryAdvancedAppIntegrationTest.groovy b/src/integTest/groovy/org/gradle/playframework/application/advanced/PlayBinaryAdvancedAppIntegrationTest.groovy index 29aef86d..4de3b9b6 100644 --- a/src/integTest/groovy/org/gradle/playframework/application/advanced/PlayBinaryAdvancedAppIntegrationTest.groovy +++ b/src/integTest/groovy/org/gradle/playframework/application/advanced/PlayBinaryAdvancedAppIntegrationTest.groovy @@ -4,12 +4,8 @@ import org.gradle.playframework.application.PlayApplicationPluginIntegrationTest import org.gradle.playframework.fixtures.app.AdvancedPlayApp import org.gradle.playframework.fixtures.app.PlayApp -import static org.gradle.playframework.plugins.PlayTwirlPlugin.TWIRL_COMPILE_TASK_NAME - class PlayBinaryAdvancedAppIntegrationTest extends PlayApplicationPluginIntegrationTest { - private static final TWIRL_COMPILE_TASK_PATH = ":$TWIRL_COMPILE_TASK_NAME".toString() - @Override PlayApp getPlayApp() { new AdvancedPlayApp(playVersion) @@ -32,12 +28,10 @@ class PlayBinaryAdvancedAppIntegrationTest extends PlayApplicationPluginIntegrat jar("build/libs/${playApp.name}-assets.jar").containsDescendants( "public/javascripts/sample.js", - "public/javascripts/sample.min.js" + "public/javascripts/sample.min.js", + "public/stylesheets/main.css", + "public/stylesheets/extra.css", + "public/lib/css-reset/reset.css", ) } - - @Override - String[] getBuildTasks() { - return super.getBuildTasks() + TWIRL_COMPILE_TASK_PATH - } } diff --git a/src/integTest/groovy/org/gradle/playframework/plugins/PlayLessPluginIntegrationTest.groovy b/src/integTest/groovy/org/gradle/playframework/plugins/PlayLessPluginIntegrationTest.groovy new file mode 100644 index 00000000..fa8c9d74 --- /dev/null +++ b/src/integTest/groovy/org/gradle/playframework/plugins/PlayLessPluginIntegrationTest.groovy @@ -0,0 +1,77 @@ +package org.gradle.playframework.plugins + +import org.gradle.playframework.AbstractIntegrationTest + +import static org.gradle.playframework.fixtures.file.FileFixtures.findFile +import static org.gradle.playframework.fixtures.Repositories.playRepositories +import static org.gradle.playframework.plugins.PlayLessPlugin.LESS_COMPILE_TASK_NAME + +class PlayLessPluginIntegrationTest extends AbstractIntegrationTest { + + def setup() { + buildFile << """ + plugins { + id 'org.gradle.playframework' + id 'org.gradle.playframework-less' + } + + ${playRepositories()} + """ + } + + def "can compile LESS files"() { + given: + File lessDir = temporaryFolder.newFolder('app', 'assets', 'stylesheets') + new File(lessDir, 'main.less') << lessSource() + + when: + build(LESS_COMPILE_TASK_NAME) + + then: + File outputDir = file('build/src/play/less') + outputDir.isDirectory() + + File[] cssFiles = new File(outputDir, 'stylesheets').listFiles() + cssFiles.length == 1 + findFile(cssFiles, 'main.css') + } + + def "can add source directories to default source set"() { + given: + File lessDir = temporaryFolder.newFolder('app', 'assets', 'stylesheets') + new File(lessDir, 'main.less') << lessSource() + + File extraLessDir = temporaryFolder.newFolder('extra', 'less', 'stylesheets') + new File(extraLessDir, 'extra.less') << lessSource() + + buildFile << """ + sourceSets { + main { + less { + srcDir 'extra/less' + } + } + } + """ + + when: + build(LESS_COMPILE_TASK_NAME) + + then: + File outputDir = file('build/src/play/less') + outputDir.isDirectory() + + File[] cssFiles = new File(outputDir, 'stylesheets').listFiles() + cssFiles.length == 2 + findFile(cssFiles, 'main.css') + findFile(cssFiles, 'extra.css') + } + + static String lessSource() { + """ + .some-class { + float: left; + } + """ + } +} diff --git a/src/integTest/groovy/org/gradle/playframework/plugins/PlayLessWithWebJarsPluginIntegrationTest.groovy b/src/integTest/groovy/org/gradle/playframework/plugins/PlayLessWithWebJarsPluginIntegrationTest.groovy new file mode 100644 index 00000000..6cbdc5c7 --- /dev/null +++ b/src/integTest/groovy/org/gradle/playframework/plugins/PlayLessWithWebJarsPluginIntegrationTest.groovy @@ -0,0 +1,53 @@ +package org.gradle.playframework.plugins + +import org.gradle.playframework.AbstractIntegrationTest + +import static org.gradle.playframework.fixtures.Repositories.playRepositories +import static org.gradle.playframework.fixtures.file.FileFixtures.findFile +import static org.gradle.playframework.plugins.PlayLessPlugin.LESS_COMPILE_TASK_NAME + +class PlayLessWithWebJarsPluginIntegrationTest extends AbstractIntegrationTest { + + def setup() { + buildFile << """ + plugins { + id 'org.gradle.playframework' + id 'org.gradle.playframework-less' + id 'org.gradle.playframework-webjars' + } + + ${playRepositories()} + + dependencies { + webJar 'org.webjars.bower:css-reset:2.5.1' + } + """ + } + + def "can compile LESS files which import files in WebJars"() { + given: + File lessDir = temporaryFolder.newFolder('app', 'assets', 'stylesheets') + new File(lessDir, 'main.less') << lessSource() + + when: + build(LESS_COMPILE_TASK_NAME) + + then: + File outputDir = file('build/src/play/less') + outputDir.isDirectory() + + File[] cssFiles = new File(outputDir, 'stylesheets').listFiles() + cssFiles.length == 1 + findFile(cssFiles, 'main.css') + } + + static String lessSource() { + """ + @import (inline) "lib/css-reset/reset.css"; + + .some-class { + float: left; + } + """ + } +} diff --git a/src/integTest/groovy/org/gradle/playframework/plugins/PlayWebJarsPluginIntegrationTest.groovy b/src/integTest/groovy/org/gradle/playframework/plugins/PlayWebJarsPluginIntegrationTest.groovy new file mode 100644 index 00000000..8bdb2f0d --- /dev/null +++ b/src/integTest/groovy/org/gradle/playframework/plugins/PlayWebJarsPluginIntegrationTest.groovy @@ -0,0 +1,40 @@ +package org.gradle.playframework.plugins + +import org.gradle.playframework.AbstractIntegrationTest + +import static org.gradle.playframework.fixtures.Repositories.playRepositories +import static org.gradle.playframework.fixtures.file.FileFixtures.findFile +import static org.gradle.playframework.plugins.PlayWebJarsPlugin.WEBJARS_EXTRACT_TASK_NAME + +class PlayWebJarsPluginIntegrationTest extends AbstractIntegrationTest { + + def setup() { + buildFile << """ + plugins { + id 'org.gradle.playframework' + id 'org.gradle.playframework-webjars' + } + + ${playRepositories()} + + dependencies { + webJar 'org.webjars:requirejs:2.3.6' + webJar 'org.webjars.npm:inherits:2.0.4' + } + """ + } + + def "can extract WebJars"() { + when: + build(WEBJARS_EXTRACT_TASK_NAME) + + then: + File outputDir = file('build/src/play/webJars/lib') + outputDir.isDirectory() + + File[] libs = outputDir.listFiles() + libs.length == 2 + findFile(libs, 'requirejs') + findFile(libs, 'inherits') + } +} diff --git a/src/integTest/groovy/org/gradle/playframework/tasks/AbstractAssetsTaskIntegrationTest.groovy b/src/integTest/groovy/org/gradle/playframework/tasks/AbstractAssetsTaskIntegrationTest.groovy new file mode 100644 index 00000000..1d6f8a6f --- /dev/null +++ b/src/integTest/groovy/org/gradle/playframework/tasks/AbstractAssetsTaskIntegrationTest.groovy @@ -0,0 +1,34 @@ +package org.gradle.playframework.tasks + +import org.gradle.playframework.AbstractIntegrationTest +import org.gradle.playframework.fixtures.archive.JarTestFixture + +import static org.gradle.api.plugins.JavaPlugin.JAR_TASK_NAME +import static org.gradle.playframework.plugins.PlayApplicationPlugin.ASSETS_JAR_TASK_NAME + +abstract class AbstractAssetsTaskIntegrationTest extends AbstractIntegrationTest { + static final JAR_TASK_PATH = ":$JAR_TASK_NAME".toString() + static final ASSETS_JAR_TASK_PATH = ":$ASSETS_JAR_TASK_NAME".toString() + + JarTestFixture jar(String fileName) { + new JarTestFixture(file(fileName)) + } + + File assets(String fileName) { + File assetsDir = file('app/assets') + + if (!assetsDir.isDirectory()) { + temporaryFolder.newFolder('app', 'assets') + } + + new File(assetsDir, fileName) + } + + boolean compareWithoutWhiteSpace(String string1, String string2) { + return withoutWhiteSpace(string1) == withoutWhiteSpace(string2) + } + + def withoutWhiteSpace(String string) { + return string.replaceAll("\\s+", " "); + } +} \ No newline at end of file diff --git a/src/integTest/groovy/org/gradle/playframework/tasks/AbstractJavaScriptMinifyIntegrationTest.groovy b/src/integTest/groovy/org/gradle/playframework/tasks/AbstractJavaScriptMinifyIntegrationTest.groovy index 021845fc..69f8294d 100644 --- a/src/integTest/groovy/org/gradle/playframework/tasks/AbstractJavaScriptMinifyIntegrationTest.groovy +++ b/src/integTest/groovy/org/gradle/playframework/tasks/AbstractJavaScriptMinifyIntegrationTest.groovy @@ -1,10 +1,9 @@ package org.gradle.playframework.tasks -import org.gradle.playframework.AbstractIntegrationTest import org.gradle.playframework.fixtures.archive.JarTestFixture import org.gradle.util.TextUtil -abstract class AbstractJavaScriptMinifyIntegrationTest extends AbstractIntegrationTest { +abstract class AbstractJavaScriptMinifyIntegrationTest extends AbstractAssetsTaskIntegrationTest { def setup() { settingsFile << """ rootProject.name = 'js-play-app' """ @@ -16,20 +15,6 @@ abstract class AbstractJavaScriptMinifyIntegrationTest extends AbstractIntegrati jar("build/libs/js-play-app-assets.jar") } - JarTestFixture jar(String fileName) { - new JarTestFixture(file(fileName)) - } - - File assets(String fileName) { - File assetsDir = file('app/assets') - - if (!assetsDir.isDirectory()) { - temporaryFolder.newFolder('app', 'assets') - } - - new File(assetsDir, fileName) - } - File processedJavaScript(String fileName) { new File(getProcessedJavaScriptDir(), fileName) } @@ -44,14 +29,6 @@ abstract class AbstractJavaScriptMinifyIntegrationTest extends AbstractIntegrati assert compareWithoutWhiteSpace(file.text, expectedJavaScript()) } - boolean compareWithoutWhiteSpace(String string1, String string2) { - return withoutWhiteSpace(string1) == withoutWhiteSpace(string2) - } - - def withoutWhiteSpace(String string) { - return string.replaceAll("\\s+", " "); - } - def withJavaScriptSource(String path) { withJavaScriptSource(file(path)) } diff --git a/src/integTest/groovy/org/gradle/playframework/tasks/JavaScriptMinifyIntegrationTest.groovy b/src/integTest/groovy/org/gradle/playframework/tasks/JavaScriptMinifyIntegrationTest.groovy index 11d56c58..fd1f13f8 100644 --- a/src/integTest/groovy/org/gradle/playframework/tasks/JavaScriptMinifyIntegrationTest.groovy +++ b/src/integTest/groovy/org/gradle/playframework/tasks/JavaScriptMinifyIntegrationTest.groovy @@ -4,15 +4,11 @@ import org.gradle.testkit.runner.BuildResult import org.gradle.testkit.runner.TaskOutcome import static org.gradle.playframework.fixtures.Repositories.playRepositories -import static org.gradle.playframework.plugins.PlayApplicationPlugin.ASSETS_JAR_TASK_NAME import static org.gradle.playframework.plugins.PlayJavaScriptPlugin.JS_MINIFY_TASK_NAME -import static org.gradle.api.plugins.JavaPlugin.JAR_TASK_NAME class JavaScriptMinifyIntegrationTest extends AbstractJavaScriptMinifyIntegrationTest { private static final JS_MINIFY_TASK_PATH = ":$JS_MINIFY_TASK_NAME".toString() - private static final JAR_TASK_PATH = ":$JAR_TASK_NAME".toString() - private static final ASSETS_JAR_TASK_PATH = ":$ASSETS_JAR_TASK_NAME".toString() File getProcessedJavaScriptDir() { file("build/src/play/javaScript") diff --git a/src/integTest/groovy/org/gradle/playframework/tasks/LessCompileIntegrationTest.groovy b/src/integTest/groovy/org/gradle/playframework/tasks/LessCompileIntegrationTest.groovy new file mode 100644 index 00000000..016c86ae --- /dev/null +++ b/src/integTest/groovy/org/gradle/playframework/tasks/LessCompileIntegrationTest.groovy @@ -0,0 +1,188 @@ +package org.gradle.playframework.tasks + +import org.gradle.playframework.fixtures.archive.JarTestFixture +import org.gradle.testkit.runner.BuildResult +import org.gradle.testkit.runner.TaskOutcome + +import static org.gradle.playframework.fixtures.Repositories.playRepositories +import static org.gradle.playframework.plugins.PlayLessPlugin.LESS_COMPILE_TASK_NAME + +class LessCompileIntegrationTest extends AbstractAssetsTaskIntegrationTest { + private static final LESS_COMPILE_TASK_PATH = ":$LESS_COMPILE_TASK_NAME".toString() + + def setup() { + settingsFile << """ rootProject.name = 'less-play-app' """ + + buildFile << """ + plugins { + id 'org.gradle.playframework' + id 'org.gradle.playframework-less' + } + + ${playRepositories()} + """ + } + + def "compiles default less source set as part of Play application build"() { + given: + withMainLessSource(assets("main.less")) + withLessSource(assets("_partial.less")) + + when: + BuildResult result = build "assemble" + + then: + result.task(LESS_COMPILE_TASK_PATH).outcome == TaskOutcome.SUCCESS + result.task(JAR_TASK_PATH).outcome == TaskOutcome.SUCCESS + result.task(ASSETS_JAR_TASK_PATH).outcome == TaskOutcome.SUCCESS + assetsJar.containsDescendants( + "public/main.css" + ) + + and: + hasProcessedMainCss("main") + } + + def "does not recompile when inputs and outputs are unchanged"() { + given: + withMainLessSource(assets("main.less")) + withLessSource(assets("_partial.less")) + build "assemble" + + when: + BuildResult result = build "assemble" + + then: + result.task(LESS_COMPILE_TASK_PATH).outcome == TaskOutcome.UP_TO_DATE + result.task(JAR_TASK_PATH).outcome == TaskOutcome.UP_TO_DATE + result.task(ASSETS_JAR_TASK_PATH).outcome == TaskOutcome.UP_TO_DATE + } + + def "recompiles when an output is removed" () { + given: + withMainLessSource(assets("main.less")) + withLessSource(assets("_partial.less")) + build "assemble" + + when: + processedCss("main").delete() + assetsJar.file.delete() + BuildResult result = build "assemble" + + then: + result.task(LESS_COMPILE_TASK_PATH).outcome == TaskOutcome.SUCCESS + result.task(ASSETS_JAR_TASK_PATH).outcome == TaskOutcome.SUCCESS + hasProcessedMainCss("main") + } + + def "recompiles when an input is changed" () { + given: + withMainLessSource(assets("main.less")) + withLessSource(assets("_partial.less")) + build "assemble" + + when: + file("app/assets/main.less") << ".other-class { margin: auto; }" + BuildResult result = build "assemble" + + then: + result.task(LESS_COMPILE_TASK_PATH).outcome == TaskOutcome.SUCCESS + result.task(ASSETS_JAR_TASK_PATH).outcome == TaskOutcome.SUCCESS + } + + def "cleans removed source file on compile" () { + given: + withMainLessSource(assets("main.less")) + withLessSource(assets("_partial.less")) + def source2 = withLessSource(assets("extra.less")) + + when: + build "assemble" + + then: + hasProcessedMainCss("main") + hasProcessedCss("extra") + assetsJar.containsDescendants( + "public/main.css", + "public/extra.css", + ) + + when: + source2.delete() + build "assemble" + + then: + ! processedCss("extra").exists() + assetsJar.countFiles("public/extra.css") == 0 + } + + def "produces sensible error on compile failure"() { + given: + assets("main.less") << "BAD SOURCE" + + when: + BuildResult result = buildAndFail "assemble" + + then: + result.output.contains("Execution failed for task ':compilePlayLess'.") + result.output.contains("Could not compile less.") + String slash = File.separator + result.output.contains("app${slash}assets${slash}main.less 1:11") + } + + def withMainLessSource(File file) { + file << """ + @import _partial; + + .class1 { + float: left; + + .class2 { + float: right; + } + } + """ + } + + def withLessSource(File file) { + file << """ + .class3 { + margin: auto; + } + """ + } + + JarTestFixture getAssetsJar() { + jar("build/libs/less-play-app-assets.jar") + } + + File processedCss(String fileName) { + new File(file("build/src/play/less"), "${fileName}.css") + } + + void hasProcessedMainCss(String fileName) { + hasExpectedMainCss(processedCss(fileName)) + } + + void hasProcessedCss(String fileName) { + hasExpectedCss(processedCss(fileName)) + } + + void hasExpectedMainCss(File file) { + assert file.exists() + assert compareWithoutWhiteSpace(file.text.readLines().get(0), expectedMainCss()) + } + + void hasExpectedCss(File file) { + assert file.exists() + assert compareWithoutWhiteSpace(file.text.readLines().get(0), expectedCss()) + } + + String expectedMainCss() { + """.class3{margin:auto;} .class1{float:left;} .class1 .class2{float:right;}""" + } + + String expectedCss() { + """.class3{margin:auto;}""" + } +} diff --git a/src/integTest/groovy/org/gradle/playframework/tasks/WebJarsExtractIntegrationTest.groovy b/src/integTest/groovy/org/gradle/playframework/tasks/WebJarsExtractIntegrationTest.groovy new file mode 100644 index 00000000..1403f986 --- /dev/null +++ b/src/integTest/groovy/org/gradle/playframework/tasks/WebJarsExtractIntegrationTest.groovy @@ -0,0 +1,77 @@ +package org.gradle.playframework.tasks + +import org.gradle.playframework.fixtures.archive.JarTestFixture +import org.gradle.testkit.runner.BuildResult +import org.gradle.testkit.runner.TaskOutcome + +import static org.gradle.playframework.fixtures.Repositories.playRepositories +import static org.gradle.playframework.plugins.PlayWebJarsPlugin.WEBJARS_EXTRACT_TASK_NAME + +class WebJarsExtractIntegrationTest extends AbstractAssetsTaskIntegrationTest { + private static final WEBJARS_EXTRACT_TASK_PATH = ":$WEBJARS_EXTRACT_TASK_NAME".toString() + + def setup() { + settingsFile << """ rootProject.name = 'webjars-play-app' """ + + buildFile << """ + plugins { + id 'org.gradle.playframework' + id 'org.gradle.playframework-webjars' + } + + ${playRepositories()} + + dependencies { + webJar 'org.webjars.bower:css-reset:2.5.1' + } + """ + } + + def "extracts WebJars as part of Play application build"() { + when: + BuildResult result = build "assemble" + + then: + result.task(WEBJARS_EXTRACT_TASK_PATH).outcome == TaskOutcome.SUCCESS + result.task(JAR_TASK_PATH).outcome == TaskOutcome.SUCCESS + result.task(ASSETS_JAR_TASK_PATH).outcome == TaskOutcome.SUCCESS + assetsJar.containsDescendants( + "public/lib/css-reset/reset.css" + ) + } + + def "does not reextract when outputs are unchanged"() { + given: + build "assemble" + + when: + BuildResult result = build "assemble" + + then: + result.task(WEBJARS_EXTRACT_TASK_PATH).outcome == TaskOutcome.UP_TO_DATE + result.task(JAR_TASK_PATH).outcome == TaskOutcome.UP_TO_DATE + result.task(ASSETS_JAR_TASK_PATH).outcome == TaskOutcome.UP_TO_DATE + } + + def "reextracts when an output is removed" () { + given: + build "assemble" + + when: + extractedWebJar("lib/css-reset/reset.css").delete() + assetsJar.file.delete() + BuildResult result = build "assemble" + + then: + result.task(WEBJARS_EXTRACT_TASK_PATH).outcome == TaskOutcome.SUCCESS + result.task(ASSETS_JAR_TASK_PATH).outcome == TaskOutcome.SUCCESS + } + + JarTestFixture getAssetsJar() { + jar("build/libs/webjars-play-app-assets.jar") + } + + File extractedWebJar(String fileName) { + new File(file("build/src/play/webJars"), fileName) + } +} diff --git a/src/integTestFixtures/resources/org/gradle/playframework/fixtures/app/advancedplayapp/app/assets/stylesheets/extra.less b/src/integTestFixtures/resources/org/gradle/playframework/fixtures/app/advancedplayapp/app/assets/stylesheets/extra.less new file mode 100644 index 00000000..420f9b0a --- /dev/null +++ b/src/integTestFixtures/resources/org/gradle/playframework/fixtures/app/advancedplayapp/app/assets/stylesheets/extra.less @@ -0,0 +1,3 @@ +.extra { + padding-top: 10px; +} diff --git a/src/integTestFixtures/resources/org/gradle/playframework/fixtures/app/advancedplayapp/app/views/main.scala.html b/src/integTestFixtures/resources/org/gradle/playframework/fixtures/app/advancedplayapp/app/views/main.scala.html index 96aabbea..bc2c3ea4 100644 --- a/src/integTestFixtures/resources/org/gradle/playframework/fixtures/app/advancedplayapp/app/views/main.scala.html +++ b/src/integTestFixtures/resources/org/gradle/playframework/fixtures/app/advancedplayapp/app/views/main.scala.html @@ -4,6 +4,7 @@ @title @** + @** **@ diff --git a/src/integTestFixtures/resources/org/gradle/playframework/fixtures/app/advancedplayapp/build.gradle b/src/integTestFixtures/resources/org/gradle/playframework/fixtures/app/advancedplayapp/build.gradle index fa5fc707..890eb6d2 100644 --- a/src/integTestFixtures/resources/org/gradle/playframework/fixtures/app/advancedplayapp/build.gradle +++ b/src/integTestFixtures/resources/org/gradle/playframework/fixtures/app/advancedplayapp/build.gradle @@ -1,5 +1,7 @@ plugins { id 'org.gradle.playframework' + id 'org.gradle.playframework-less' + id 'org.gradle.playframework-webjars' } // repositories added in PlayApp class @@ -15,6 +17,8 @@ sourceSets { } dependencies { + webJar 'org.webjars.bower:css-reset:2.5.1' + implementation "com.typesafe.play:play-guice_2.12:2.6.15" implementation "ch.qos.logback:logback-classic:1.2.3" } diff --git a/src/main/java/org/gradle/playframework/plugins/PlayApplicationPlugin.java b/src/main/java/org/gradle/playframework/plugins/PlayApplicationPlugin.java index e2a4f86e..b7c4d3b8 100644 --- a/src/main/java/org/gradle/playframework/plugins/PlayApplicationPlugin.java +++ b/src/main/java/org/gradle/playframework/plugins/PlayApplicationPlugin.java @@ -48,6 +48,7 @@ public class PlayApplicationPlugin implements Plugin { static final String PLAY_EXTENSION_NAME = "play"; static final String PLATFORM_CONFIGURATION = "play"; + public static final String PLAY_RUN_TASK_NAME = "runPlay"; public static final String ASSETS_JAR_TASK_NAME = "createPlayAssetsJar"; @Override @@ -152,7 +153,7 @@ private static TaskProvider getRoutesCompileTask(Project project) } private TaskProvider createRunTask(Project project, PlayExtension playExtension, TaskProvider mainJarTask, TaskProvider assetsJarTask) { - return project.getTasks().register("runPlay", PlayRun.class, playRun -> { + return project.getTasks().register(PLAY_RUN_TASK_NAME, PlayRun.class, playRun -> { playRun.setDescription("Runs the Play application for local development."); playRun.setGroup("Run"); playRun.getWorkingDir().convention(project.getLayout().getProjectDirectory()); diff --git a/src/main/java/org/gradle/playframework/plugins/PlayJavaScriptPlugin.java b/src/main/java/org/gradle/playframework/plugins/PlayJavaScriptPlugin.java index ffdf30ec..05669938 100644 --- a/src/main/java/org/gradle/playframework/plugins/PlayJavaScriptPlugin.java +++ b/src/main/java/org/gradle/playframework/plugins/PlayJavaScriptPlugin.java @@ -1,14 +1,18 @@ package org.gradle.playframework.plugins; +import org.gradle.api.tasks.TaskProvider; import org.gradle.playframework.plugins.internal.PlayPluginHelper; import org.gradle.playframework.sourcesets.JavaScriptSourceSet; import org.gradle.playframework.sourcesets.internal.DefaultJavaScriptSourceSet; import org.gradle.playframework.tasks.JavaScriptMinify; +import org.gradle.playframework.tasks.PlayRun; import org.gradle.playframework.tools.internal.javascript.GoogleClosureCompiler; import org.gradle.api.Project; import org.gradle.api.artifacts.Configuration; import org.gradle.api.file.SourceDirectorySet; +import static org.gradle.playframework.plugins.PlayApplicationPlugin.PLAY_RUN_TASK_NAME; + /** * Plugin for adding javascript processing to a Play application. */ @@ -38,12 +42,15 @@ private void declareDefaultDependencies(Project project, Configuration configura } private void createDefaultJavaScriptMinifyTask(Project project, SourceDirectorySet sourceDirectory, Configuration compilerConfiguration) { - project.getTasks().register(JS_MINIFY_TASK_NAME, JavaScriptMinify.class, javaScriptMinify -> { + TaskProvider javaScriptMinifyTask = project.getTasks().register(JS_MINIFY_TASK_NAME, JavaScriptMinify.class, javaScriptMinify -> { javaScriptMinify.setDescription("Minifies javascript for the " + sourceDirectory.getDisplayName() + "."); javaScriptMinify.getDestinationDir().set(getOutputDir(project, sourceDirectory)); javaScriptMinify.setSource(sourceDirectory); javaScriptMinify.getCompilerClasspath().setFrom(compilerConfiguration); javaScriptMinify.dependsOn(sourceDirectory); }); + project.getTasks().named(PLAY_RUN_TASK_NAME, PlayRun.class, task -> { + task.getAssetsDirs().from(javaScriptMinifyTask.get().getDestinationDir()); + }); } } diff --git a/src/main/java/org/gradle/playframework/plugins/PlayLessPlugin.java b/src/main/java/org/gradle/playframework/plugins/PlayLessPlugin.java new file mode 100644 index 00000000..083d44a3 --- /dev/null +++ b/src/main/java/org/gradle/playframework/plugins/PlayLessPlugin.java @@ -0,0 +1,72 @@ +package org.gradle.playframework.plugins; + +import org.gradle.api.Project; +import org.gradle.api.artifacts.Configuration; +import org.gradle.api.file.SourceDirectorySet; +import org.gradle.api.tasks.TaskProvider; +import org.gradle.api.tasks.bundling.Jar; +import org.gradle.playframework.sourcesets.LessSourceSet; +import org.gradle.playframework.sourcesets.internal.DefaultLessSourceSet; +import org.gradle.playframework.tasks.LessCompile; +import org.gradle.playframework.tasks.PlayRun; +import org.gradle.playframework.tools.internal.less.Less4jCompiler; + +import static org.gradle.api.plugins.BasePlugin.ASSEMBLE_TASK_NAME; +import static org.gradle.playframework.plugins.PlayApplicationPlugin.ASSETS_JAR_TASK_NAME; +import static org.gradle.playframework.plugins.PlayApplicationPlugin.PLAY_RUN_TASK_NAME; +import static org.gradle.playframework.plugins.internal.PlayPluginHelper.createCustomSourceSet; + +/** + * Plugin for compiling LESS stylesheets into CSS stylesheets in a Play application. + */ +public class PlayLessPlugin implements PlayGeneratedSourcePlugin { + public static final String LESS_COMPILER_CONFIGURATION_NAME = "lessCompiler"; + public static final String LESS_COMPILE_TASK_NAME = "compilePlayLess"; + + @Override + public void apply(Project project) { + Configuration configuration = createLessCompilerConfiguration(project); + declareDefaultDependencies(project, configuration); + LessSourceSet sourceSet = createCustomSourceSet(project, DefaultLessSourceSet.class, "less"); + createDefaultLessCompileTask(project, sourceSet.getLess(), configuration); + configureLessCompileTask(project); + } + + private Configuration createLessCompilerConfiguration(Project project) { + Configuration configuration = project.getConfigurations().create(LESS_COMPILER_CONFIGURATION_NAME); + configuration.setVisible(false); + configuration.setTransitive(true); + configuration.setDescription("The LESS compiler library used to generate CSS stylesheets from LESS stylesheets."); + return configuration; + } + + private void declareDefaultDependencies(Project project, Configuration configuration) { + configuration.defaultDependencies(dependencies -> { + dependencies.add(project.getDependencies().create(Less4jCompiler.getDependencyNotation())); + }); + } + + private void createDefaultLessCompileTask(Project project, SourceDirectorySet sourceDirectory, Configuration configuration) { + project.getTasks().register(LESS_COMPILE_TASK_NAME, LessCompile.class, task -> { + task.setDescription("Generates CSS stylesheets for the '" + sourceDirectory.getDisplayName() + "' source set."); + task.setSource(sourceDirectory); + task.getOutputDirectory().set(getOutputDir(project, sourceDirectory)); + task.getLessCompilerClasspath().setFrom(configuration); + }); + } + + private void configureLessCompileTask(Project project) { + TaskProvider lessCompileTaskProvider = project.getTasks().named(LESS_COMPILE_TASK_NAME, LessCompile.class); + + project.getTasks().named(ASSEMBLE_TASK_NAME, task -> { + task.dependsOn(lessCompileTaskProvider); + }); + project.getTasks().named(ASSETS_JAR_TASK_NAME, Jar.class, task -> { + task.dependsOn(lessCompileTaskProvider); + task.from(lessCompileTaskProvider.get().getOutputDirectory(), copySpec -> copySpec.into("public")); + }); + project.getTasks().named(PLAY_RUN_TASK_NAME, PlayRun.class, task -> { + task.getAssetsDirs().from(lessCompileTaskProvider.get().getOutputDirectory()); + }); + } +} diff --git a/src/main/java/org/gradle/playframework/plugins/PlayWebJarsPlugin.java b/src/main/java/org/gradle/playframework/plugins/PlayWebJarsPlugin.java new file mode 100644 index 00000000..ce752895 --- /dev/null +++ b/src/main/java/org/gradle/playframework/plugins/PlayWebJarsPlugin.java @@ -0,0 +1,81 @@ +package org.gradle.playframework.plugins; + +import org.gradle.api.Project; +import org.gradle.api.artifacts.Configuration; +import org.gradle.api.tasks.TaskProvider; +import org.gradle.api.tasks.bundling.Jar; +import org.gradle.playframework.tasks.LessCompile; +import org.gradle.playframework.tasks.PlayRun; +import org.gradle.playframework.tasks.WebJarsExtract; +import org.gradle.playframework.tools.internal.webjars.WebJarsExtractor; + +import static org.gradle.api.plugins.BasePlugin.ASSEMBLE_TASK_NAME; +import static org.gradle.playframework.plugins.PlayApplicationPlugin.ASSETS_JAR_TASK_NAME; +import static org.gradle.playframework.plugins.PlayApplicationPlugin.PLAY_RUN_TASK_NAME; +import static org.gradle.playframework.plugins.PlayLessPlugin.LESS_COMPILE_TASK_NAME; + +/** + * Plugin for extracting WebJars in a Play application. + */ +public class PlayWebJarsPlugin implements PlayGeneratedSourcePlugin { + public static final String WEBJAR_CONFIGURATION_NAME = "webJar"; + public static final String WEBJARS_EXTRACTOR_CONFIGURATION_NAME = "webJarsExtractor"; + public static final String WEBJARS_EXTRACT_TASK_NAME = "extractPlayWebJars"; + + @Override + public void apply(Project project) { + createWebJarConfiguration(project); + Configuration webJarsExtractorConfiguration = createWebJarsExtractorConfiguration(project); + declareDefaultDependencies(project, webJarsExtractorConfiguration); + createDefaultWebJarsExtractTask(project, webJarsExtractorConfiguration); + configureWebJarsExtractTask(project); + } + + private void createWebJarConfiguration(Project project) { + Configuration configuration = project.getConfigurations().create(WEBJAR_CONFIGURATION_NAME); + configuration.setTransitive(true); + } + + private Configuration createWebJarsExtractorConfiguration(Project project) { + Configuration configuration = project.getConfigurations().create(WEBJARS_EXTRACTOR_CONFIGURATION_NAME); + configuration.setVisible(false); + configuration.setTransitive(true); + configuration.setDescription("The WebJars extractor library used to extract WebJars."); + return configuration; + } + + private void declareDefaultDependencies(Project project, Configuration configuration) { + configuration.defaultDependencies(dependencies -> { + dependencies.add(project.getDependencies().create(WebJarsExtractor.getDependencyNotation())); + }); + } + + private void createDefaultWebJarsExtractTask(Project project, Configuration configuration) { + project.getTasks().register(WEBJARS_EXTRACT_TASK_NAME, WebJarsExtract.class, task -> { + task.setDescription("Extracts WebJars."); + task.getOutputDirectory().set(project.getLayout().getBuildDirectory().dir(GENERATED_SOURCE_ROOT_DIR_PATH + "/webJars")); + task.getWebJarsClasspath().setFrom(project.getConfigurations().getByName(WEBJAR_CONFIGURATION_NAME)); + task.getWebJarsExtractorClasspath().setFrom(configuration); + }); + } + + private void configureWebJarsExtractTask(Project project) { + TaskProvider webJarsExtractTaskProvider = project.getTasks().named(WEBJARS_EXTRACT_TASK_NAME, WebJarsExtract.class); + + project.getTasks().named(ASSEMBLE_TASK_NAME, task -> { + task.dependsOn(webJarsExtractTaskProvider); + }); + project.getTasks().named(ASSETS_JAR_TASK_NAME, Jar.class, task -> { + task.dependsOn(webJarsExtractTaskProvider); + task.from(webJarsExtractTaskProvider.get().getOutputDirectory(), copySpec -> copySpec.into("public")); + }); + project.getTasks().named(PLAY_RUN_TASK_NAME, PlayRun.class, task -> { + task.getAssetsDirs().from(webJarsExtractTaskProvider.get().getOutputDirectory()); + }); + project.getTasks().matching(task -> task.getName().equals(LESS_COMPILE_TASK_NAME)).forEach(task -> { + LessCompile lessCompileTask = (LessCompile) task; + lessCompileTask.dependsOn(webJarsExtractTaskProvider); + lessCompileTask.getIncludePaths().add(webJarsExtractTaskProvider.get().getOutputDirectory().get().getAsFile()); + }); + } +} diff --git a/src/main/java/org/gradle/playframework/sourcesets/LessSourceSet.java b/src/main/java/org/gradle/playframework/sourcesets/LessSourceSet.java new file mode 100644 index 00000000..80e3037a --- /dev/null +++ b/src/main/java/org/gradle/playframework/sourcesets/LessSourceSet.java @@ -0,0 +1,38 @@ +package org.gradle.playframework.sourcesets; + +import org.gradle.api.Action; +import org.gradle.api.file.SourceDirectorySet; + +/** + * Represents a source set containing LESS sources. + *

+ * The following example demonstrate the use of the source set in a build script using the Groovy DSL: + *

+ * sourceSets {
+ *     main {
+ *         less {
+ *             srcDir 'app/assets'
+ *             include '{@literal **}/*.less'
+ *             exclude '{@literal **}/_*.less'
+ *         }
+ *     }
+ * }
+ * 
+ */ +public interface LessSourceSet { + + /** + * Returns the source directory set. + * + * @return The source directory set + */ + SourceDirectorySet getLess(); + + /** + * Configures the source set. + * + * @param configureAction The configuration action + * @return The source set + */ + LessSourceSet less(Action configureAction); +} diff --git a/src/main/java/org/gradle/playframework/sourcesets/internal/DefaultLessSourceSet.java b/src/main/java/org/gradle/playframework/sourcesets/internal/DefaultLessSourceSet.java new file mode 100644 index 00000000..4741abda --- /dev/null +++ b/src/main/java/org/gradle/playframework/sourcesets/internal/DefaultLessSourceSet.java @@ -0,0 +1,32 @@ +package org.gradle.playframework.sourcesets.internal; + +import org.gradle.api.Action; +import org.gradle.api.file.SourceDirectorySet; +import org.gradle.api.model.ObjectFactory; +import org.gradle.playframework.sourcesets.LessSourceSet; + +import javax.inject.Inject; + +public class DefaultLessSourceSet implements LessSourceSet { + + private final SourceDirectorySet less; + + @Inject + public DefaultLessSourceSet(String name, String displayName, ObjectFactory objectFactory) { + less = objectFactory.sourceDirectorySet(name, displayName + " LESS source"); + less.srcDirs("app/assets"); + less.include("**/*.less"); + less.exclude("**/_*.less"); + } + + @Override + public SourceDirectorySet getLess() { + return less; + } + + @Override + public LessSourceSet less(Action configureAction) { + configureAction.execute(getLess()); + return this; + } +} diff --git a/src/main/java/org/gradle/playframework/tasks/LessCompile.java b/src/main/java/org/gradle/playframework/tasks/LessCompile.java new file mode 100644 index 00000000..f82909e3 --- /dev/null +++ b/src/main/java/org/gradle/playframework/tasks/LessCompile.java @@ -0,0 +1,90 @@ +package org.gradle.playframework.tasks; + +import org.gradle.api.file.ConfigurableFileCollection; +import org.gradle.api.file.Directory; +import org.gradle.api.file.FileTree; +import org.gradle.api.file.FileVisitDetails; +import org.gradle.api.file.FileVisitor; +import org.gradle.api.internal.file.RelativeFile; +import org.gradle.api.provider.Property; +import org.gradle.api.tasks.Classpath; +import org.gradle.api.tasks.OutputDirectory; +import org.gradle.api.tasks.PathSensitive; +import org.gradle.api.tasks.PathSensitivity; +import org.gradle.api.tasks.SourceTask; +import org.gradle.api.tasks.TaskAction; +import org.gradle.playframework.tasks.internal.LessCompileRunnable; +import org.gradle.playframework.tools.internal.less.DefaultLessCompileSpec; +import org.gradle.playframework.tools.internal.less.Less4jCompiler; +import org.gradle.playframework.tools.internal.less.LessCompileSpec; +import org.gradle.workers.IsolationMode; +import org.gradle.workers.WorkerExecutor; + +import javax.inject.Inject; +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +public class LessCompile extends SourceTask { + private final WorkerExecutor workerExecutor; + private final Property outputDirectory; + private final ConfigurableFileCollection lessCompilerClasspath; + private final List lessCompilerIncludePaths; + + @Inject + public LessCompile(WorkerExecutor workerExecutor) { + this.workerExecutor = workerExecutor; + this.outputDirectory = getProject().getObjects().directoryProperty(); + this.lessCompilerClasspath = getProject().files(); + this.lessCompilerIncludePaths = new ArrayList<>(); + } + + @Override + @PathSensitive(PathSensitivity.RELATIVE) + public FileTree getSource() { + return super.getSource(); + } + + @OutputDirectory + public Property getOutputDirectory() { + return outputDirectory; + } + + @Classpath + public ConfigurableFileCollection getLessCompilerClasspath() { + return lessCompilerClasspath; + } + + public List getIncludePaths() { + return lessCompilerIncludePaths; + } + + @TaskAction + void compile() { + RelativeFileCollector relativeFileCollector = new RelativeFileCollector(); + getSource().visit(relativeFileCollector); + final LessCompileSpec spec = new DefaultLessCompileSpec(relativeFileCollector.relativeFiles, getOutputDirectory().get().getAsFile(), getIncludePaths()); + + workerExecutor.submit(LessCompileRunnable.class, workerConfiguration -> { + workerConfiguration.setIsolationMode(IsolationMode.PROCESS); + workerConfiguration.forkOptions(options -> options.jvmArgs("-XX:MaxMetaspaceSize=256m")); + workerConfiguration.params(spec, new Less4jCompiler()); + workerConfiguration.classpath(lessCompilerClasspath); + workerConfiguration.setDisplayName("Generating CSS stylesheets from LESS stylesheets"); + }); + workerExecutor.await(); + } + + private static class RelativeFileCollector implements FileVisitor { + List relativeFiles = new ArrayList<>(); + + @Override + public void visitDir(FileVisitDetails dirDetails) { + } + + @Override + public void visitFile(FileVisitDetails fileDetails) { + relativeFiles.add(new RelativeFile(fileDetails.getFile(), fileDetails.getRelativePath())); + } + } +} diff --git a/src/main/java/org/gradle/playframework/tasks/WebJarsExtract.java b/src/main/java/org/gradle/playframework/tasks/WebJarsExtract.java new file mode 100644 index 00000000..9d412da8 --- /dev/null +++ b/src/main/java/org/gradle/playframework/tasks/WebJarsExtract.java @@ -0,0 +1,60 @@ +package org.gradle.playframework.tasks; + +import org.gradle.api.DefaultTask; +import org.gradle.api.file.ConfigurableFileCollection; +import org.gradle.api.file.Directory; +import org.gradle.api.provider.Property; +import org.gradle.api.tasks.InputFiles; +import org.gradle.api.tasks.OutputDirectory; +import org.gradle.api.tasks.TaskAction; +import org.gradle.playframework.tasks.internal.WebJarsExtractRunnable; +import org.gradle.playframework.tools.internal.webjars.DefaultWebJarsExtractSpec; +import org.gradle.playframework.tools.internal.webjars.WebJarsExtractSpec; +import org.gradle.workers.IsolationMode; +import org.gradle.workers.WorkerExecutor; + +import javax.inject.Inject; + +public class WebJarsExtract extends DefaultTask { + private final WorkerExecutor workerExecutor; + private final Property outputDirectory; + private final ConfigurableFileCollection webJarsClasspath; + private final ConfigurableFileCollection webJarsExtractorClasspath; + + @Inject + public WebJarsExtract(WorkerExecutor workerExecutor) { + this.workerExecutor = workerExecutor; + this.outputDirectory = getProject().getObjects().directoryProperty(); + this.webJarsClasspath = getProject().files(); + this.webJarsExtractorClasspath = getProject().files(); + } + + @OutputDirectory + public Property getOutputDirectory() { + return outputDirectory; + } + + @InputFiles + public ConfigurableFileCollection getWebJarsClasspath() { + return webJarsClasspath; + } + + public ConfigurableFileCollection getWebJarsExtractorClasspath() { + return webJarsExtractorClasspath; + } + + @TaskAction + public void extract() { + final WebJarsExtractSpec spec = new DefaultWebJarsExtractSpec(getWebJarsClasspath().getFiles(), getOutputDirectory().get().getAsFile()); + + workerExecutor.submit(WebJarsExtractRunnable.class, workerConfiguration -> { + workerConfiguration.setIsolationMode(IsolationMode.PROCESS); + workerConfiguration.forkOptions(options -> options.jvmArgs("-XX:MaxMetaspaceSize=256m")); + workerConfiguration.params(spec); + workerConfiguration.classpath(webJarsExtractorClasspath); + workerConfiguration.setDisplayName("Extracting WebJars"); + }); + + workerExecutor.await(); + } +} \ No newline at end of file diff --git a/src/main/java/org/gradle/playframework/tasks/internal/LessCompileRunnable.java b/src/main/java/org/gradle/playframework/tasks/internal/LessCompileRunnable.java new file mode 100644 index 00000000..a550d2e3 --- /dev/null +++ b/src/main/java/org/gradle/playframework/tasks/internal/LessCompileRunnable.java @@ -0,0 +1,25 @@ +package org.gradle.playframework.tasks.internal; + +import org.gradle.playframework.tools.internal.Compiler; +import org.gradle.playframework.tools.internal.less.LessCompileSpec; +import org.gradle.util.GFileUtils; + +import javax.inject.Inject; + +public class LessCompileRunnable implements Runnable { + + private final LessCompileSpec lessCompileSpec; + private final Compiler compiler; + + @Inject + public LessCompileRunnable(LessCompileSpec lessCompileSpec, Compiler compiler) { + this.lessCompileSpec = lessCompileSpec; + this.compiler = compiler; + } + + @Override + public void run() { + GFileUtils.forceDelete(lessCompileSpec.getDestinationDir()); + compiler.execute(lessCompileSpec); + } +} diff --git a/src/main/java/org/gradle/playframework/tasks/internal/WebJarsExtractRunnable.java b/src/main/java/org/gradle/playframework/tasks/internal/WebJarsExtractRunnable.java new file mode 100644 index 00000000..a6e6beb8 --- /dev/null +++ b/src/main/java/org/gradle/playframework/tasks/internal/WebJarsExtractRunnable.java @@ -0,0 +1,22 @@ +package org.gradle.playframework.tasks.internal; + +import org.gradle.playframework.tools.internal.webjars.WebJarsExtractSpec; +import org.gradle.playframework.tools.internal.webjars.WebJarsExtractor; +import org.gradle.util.GFileUtils; + +import javax.inject.Inject; + +public class WebJarsExtractRunnable implements Runnable { + private final WebJarsExtractSpec spec; + + @Inject + public WebJarsExtractRunnable(WebJarsExtractSpec spec) { + this.spec = spec; + } + + @Override + public void run() { + GFileUtils.forceDelete(spec.getDestinationDir()); + new WebJarsExtractor().execute(spec); + } +} diff --git a/src/main/java/org/gradle/playframework/tools/internal/less/DefaultLessCompileSpec.java b/src/main/java/org/gradle/playframework/tools/internal/less/DefaultLessCompileSpec.java new file mode 100644 index 00000000..30a116fe --- /dev/null +++ b/src/main/java/org/gradle/playframework/tools/internal/less/DefaultLessCompileSpec.java @@ -0,0 +1,33 @@ +package org.gradle.playframework.tools.internal.less; + +import org.gradle.api.internal.file.RelativeFile; + +import java.io.File; +import java.util.List; + +public class DefaultLessCompileSpec implements LessCompileSpec { + private final Iterable sourceFiles; + private final File destinationDir; + private final List includePaths; + + public DefaultLessCompileSpec(Iterable sourceFiles, File destinationDir, List includePaths) { + this.sourceFiles = sourceFiles; + this.destinationDir = destinationDir; + this.includePaths = includePaths; + } + + @Override + public Iterable getSources() { + return sourceFiles; + } + + @Override + public File getDestinationDir() { + return destinationDir; + } + + @Override + public List getIncludePaths() { + return includePaths; + } +} diff --git a/src/main/java/org/gradle/playframework/tools/internal/less/Less4jCompiler.java b/src/main/java/org/gradle/playframework/tools/internal/less/Less4jCompiler.java new file mode 100644 index 00000000..ebb7359b --- /dev/null +++ b/src/main/java/org/gradle/playframework/tools/internal/less/Less4jCompiler.java @@ -0,0 +1,108 @@ +package org.gradle.playframework.tools.internal.less; + +import org.gradle.api.file.RelativePath; +import org.gradle.api.internal.file.RelativeFile; +import org.gradle.api.tasks.WorkResult; +import org.gradle.api.tasks.WorkResults; +import org.gradle.playframework.tools.internal.Compiler; +import org.gradle.playframework.tools.internal.reflection.DirectInstantiator; +import org.gradle.playframework.tools.internal.reflection.JavaMethod; +import org.gradle.playframework.tools.internal.reflection.JavaReflectionUtil; +import org.gradle.util.GFileUtils; + +import java.io.File; +import java.io.Serializable; + +public class Less4jCompiler implements Compiler, Serializable { + + private Class sourceClass; + private Class multiSourceClass; + private Class configurationClass; + private Class resultClass; + private Class compilerClass; + + public static Object getDependencyNotation() { + return "com.github.sommeri:less4j:1.17.2"; + } + + @Override + public WorkResult execute(LessCompileSpec spec) { + boolean didWork = false; + File[] includePaths = spec.getIncludePaths().toArray(new File[0]); + for (RelativeFile lessFile : spec.getSources()) { + File cssFile = new File( + spec.getDestinationDir(), + toCss(lessFile.getRelativePath()).getPathString()); + + didWork |= compile(lessFile.getFile(), cssFile, includePaths); + } + return WorkResults.didWork(didWork); + } + + private boolean compile(File lessFile, File cssFile, File[] includePaths) { + loadCompilerClasses(getClass().getClassLoader()); + + Object lessSource = DirectInstantiator.INSTANCE.newInstance(multiSourceClass, lessFile, includePaths); + Object options = DirectInstantiator.INSTANCE.newInstance(configurationClass); + + JavaMethod setCssResultLocation = JavaReflectionUtil.method(configurationClass, Void.class, "setCssResultLocation", File.class); + setCssResultLocation.invoke(options, cssFile); + + JavaMethod setCompressing = JavaReflectionUtil.method(configurationClass, configurationClass, "setCompressing", boolean.class); + setCompressing.invoke(options, true); + + Object compiler = DirectInstantiator.INSTANCE.newInstance(compilerClass); + + JavaMethod doCompile = JavaReflectionUtil.method(compilerClass, Object.class, "compile", sourceClass, configurationClass); + Object result = doCompile.invoke(compiler, lessSource, options); + + JavaMethod getCss = JavaReflectionUtil.method(resultClass, String.class, "getCss"); + String css = getCss.invoke(result); + + GFileUtils.writeFile(css, cssFile); + return true; + } + + private void loadCompilerClasses(ClassLoader cl) { + try { + if (sourceClass == null) { + @SuppressWarnings("unchecked") + Class clazz = (Class) cl.loadClass("com.github.sommeri.less4j.LessSource"); + sourceClass = clazz; + } + if (multiSourceClass == null) { + @SuppressWarnings("unchecked") + Class clazz = (Class) cl.loadClass("com.github.sommeri.less4j.MultiPathFileSource"); + multiSourceClass = clazz; + } + if (configurationClass == null) { + @SuppressWarnings("unchecked") + Class clazz = (Class) cl.loadClass("com.github.sommeri.less4j.LessCompiler$Configuration"); + configurationClass = clazz; + } + if (resultClass == null) { + @SuppressWarnings("unchecked") + Class clazz = (Class) cl.loadClass("com.github.sommeri.less4j.LessCompiler$CompilationResult"); + resultClass = clazz; + } + if (compilerClass == null) { + @SuppressWarnings("unchecked") + Class clazz = (Class) cl.loadClass("com.github.sommeri.less4j.core.ThreadUnsafeLessCompiler"); + compilerClass = clazz; + } + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + } + + private static RelativePath toCss(RelativePath path) { + String lessFilename = path.getLastName(); + String cssFilename; + if (lessFilename.endsWith(".less")) { + cssFilename = lessFilename.substring(0, lessFilename.length() - ".less".length()) + ".css"; + } else { + cssFilename = lessFilename + ".css"; + } + return path.replaceLastName(cssFilename); + } +} diff --git a/src/main/java/org/gradle/playframework/tools/internal/less/LessCompileSpec.java b/src/main/java/org/gradle/playframework/tools/internal/less/LessCompileSpec.java new file mode 100644 index 00000000..01f5c412 --- /dev/null +++ b/src/main/java/org/gradle/playframework/tools/internal/less/LessCompileSpec.java @@ -0,0 +1,14 @@ +package org.gradle.playframework.tools.internal.less; + +import org.gradle.api.internal.file.RelativeFile; +import org.gradle.playframework.tools.internal.PlayCompileSpec; + +import java.io.File; +import java.io.Serializable; +import java.util.List; + +public interface LessCompileSpec extends PlayCompileSpec, Serializable { + Iterable getSources(); + + List getIncludePaths(); +} diff --git a/src/main/java/org/gradle/playframework/tools/internal/webjars/DefaultWebJarsExtractSpec.java b/src/main/java/org/gradle/playframework/tools/internal/webjars/DefaultWebJarsExtractSpec.java new file mode 100644 index 00000000..9b78e1d5 --- /dev/null +++ b/src/main/java/org/gradle/playframework/tools/internal/webjars/DefaultWebJarsExtractSpec.java @@ -0,0 +1,24 @@ +package org.gradle.playframework.tools.internal.webjars; + +import java.io.File; +import java.util.Set; + +public class DefaultWebJarsExtractSpec implements WebJarsExtractSpec { + private final Set classpath; + private final File destinationDir; + + public DefaultWebJarsExtractSpec(Set classpath, File destinationDir) { + this.classpath = classpath; + this.destinationDir = destinationDir; + } + + @Override + public Set getClasspath() { + return classpath; + } + + @Override + public File getDestinationDir() { + return destinationDir; + } +} diff --git a/src/main/java/org/gradle/playframework/tools/internal/webjars/WebJarsExtractSpec.java b/src/main/java/org/gradle/playframework/tools/internal/webjars/WebJarsExtractSpec.java new file mode 100644 index 00000000..3a96facb --- /dev/null +++ b/src/main/java/org/gradle/playframework/tools/internal/webjars/WebJarsExtractSpec.java @@ -0,0 +1,11 @@ +package org.gradle.playframework.tools.internal.webjars; + +import java.io.File; +import java.io.Serializable; +import java.util.Set; + +public interface WebJarsExtractSpec extends Serializable { + Set getClasspath(); + + File getDestinationDir(); +} diff --git a/src/main/java/org/gradle/playframework/tools/internal/webjars/WebJarsExtractor.java b/src/main/java/org/gradle/playframework/tools/internal/webjars/WebJarsExtractor.java new file mode 100644 index 00000000..682699f4 --- /dev/null +++ b/src/main/java/org/gradle/playframework/tools/internal/webjars/WebJarsExtractor.java @@ -0,0 +1,62 @@ +package org.gradle.playframework.tools.internal.webjars; + +import org.gradle.api.tasks.WorkResult; +import org.gradle.api.tasks.WorkResults; +import org.gradle.playframework.tools.internal.reflection.DirectInstantiator; +import org.gradle.playframework.tools.internal.reflection.JavaMethod; +import org.gradle.playframework.tools.internal.reflection.JavaReflectionUtil; + +import java.io.File; +import java.io.Serializable; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.ArrayList; +import java.util.List; + +public class WebJarsExtractor implements Serializable { + + private Class extractorClass; + + public static Object getDependencyNotation() { + return "org.webjars:webjars-locator-core:0.32"; + } + + public WorkResult execute(WebJarsExtractSpec spec) { + List urls = new ArrayList<>(); + + spec.getClasspath().forEach(file -> { + try { + urls.add(file.toURI().toURL()); + } catch (MalformedURLException e) { + throw new RuntimeException(e); + } + }); + + URLClassLoader classLoader = new URLClassLoader(urls.toArray(new URL[urls.size()])); + + loadCompilerClasses(getClass().getClassLoader()); + + Object extractor = DirectInstantiator.INSTANCE.newInstance(extractorClass, classLoader); + + JavaMethod extractAllWebJarsTo = JavaReflectionUtil.method(extractorClass, Void.class, "extractAllWebJarsTo", File.class); + extractAllWebJarsTo.invoke(extractor, new File(spec.getDestinationDir(), "lib")); + + JavaMethod extractAllNodeModulesTo = JavaReflectionUtil.method(extractorClass, Void.class, "extractAllNodeModulesTo", File.class); + extractAllNodeModulesTo.invoke(extractor, new File(spec.getDestinationDir(), "lib")); + + return WorkResults.didWork(true); + } + + private void loadCompilerClasses(ClassLoader cl) { + try { + if (extractorClass == null) { + @SuppressWarnings("unchecked") + Class clazz = (Class) cl.loadClass("org.webjars.WebJarExtractor"); + extractorClass = clazz; + } + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + } +}