summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGuillaume Grossetie <g.grossetie@gmail.com>2018-05-24 12:19:29 +0200
committerNikolay Tatarinov <5min4eq.unity@gmail.com>2018-05-24 13:19:29 +0300
commit02436b4000cc16683984244ca272b93c1ff015c7 (patch)
tree0fa431c28f31cc58fd513b5aa89c17fe4108607d
parentab2a8b3b9eddbdc4212e6f0e2a4a0adea1f3caee (diff)
downloadmill-02436b4000cc16683984244ca272b93c1ff015c7.tar.gz
mill-02436b4000cc16683984244ca272b93c1ff015c7.tar.bz2
mill-02436b4000cc16683984244ca272b93c1ff015c7.zip
Add a TwirlModule to compile Twirl templates (#271)
* initial implementation * Upgrade to the latest version * Add tests * Update the code to comply with the new API * Use reflection to call TwirlCompiler.compile function * Run twirllib.test on CI * Use the Java API as a workaround * wip * Cleanup the code (code review) * Add an example to call the Scala API * twirl that works with scala API * Create functions to override the default settings (will be available in the future)
-rwxr-xr-xbuild.sc6
-rwxr-xr-xci/test-mill-0.sh2
-rwxr-xr-xci/test-mill-dev.sh2
-rw-r--r--main/test/src/mill/define/CacherTests.scala2
-rw-r--r--twirllib/src/mill/twirllib/TwirlModule.scala56
-rw-r--r--twirllib/src/mill/twirllib/TwirlWorker.scala119
-rw-r--r--twirllib/test/resources/hello-world/core/views/hello.scala.html6
-rw-r--r--twirllib/test/src/mill/twirllib/HelloWorldTests.scala75
8 files changed, 265 insertions, 3 deletions
diff --git a/build.sc b/build.sc
index 86ef2d00..ac6c2ea9 100755
--- a/build.sc
+++ b/build.sc
@@ -196,6 +196,12 @@ object scalajslib extends MillModule {
}
}
+object twirllib extends MillModule {
+
+ def moduleDeps = Seq(scalalib)
+
+}
+
def testRepos = T{
Seq(
"MILL_ACYCLIC_REPO" ->
diff --git a/ci/test-mill-0.sh b/ci/test-mill-0.sh
index daae5bd6..e63760cf 100755
--- a/ci/test-mill-0.sh
+++ b/ci/test-mill-0.sh
@@ -6,4 +6,4 @@ set -eux
git clean -xdf
# Run tests
-mill -i all {main,scalalib,scalajslib,main.client}.test
+mill -i all {main,scalalib,scalajslib,twirllib,main.client}.test
diff --git a/ci/test-mill-dev.sh b/ci/test-mill-dev.sh
index 459f3eb1..f5a8bfcd 100755
--- a/ci/test-mill-dev.sh
+++ b/ci/test-mill-dev.sh
@@ -11,5 +11,5 @@ mill -i dev.assembly
rm -rf ~/.mill
# Second build & run tests
-out/dev/assembly/dest/mill -i all {main,scalalib,scalajslib}.test
+out/dev/assembly/dest/mill -i all {main,scalalib,scalajslib,twirllib}.test
diff --git a/main/test/src/mill/define/CacherTests.scala b/main/test/src/mill/define/CacherTests.scala
index 03232930..98f2b7f8 100644
--- a/main/test/src/mill/define/CacherTests.scala
+++ b/main/test/src/mill/define/CacherTests.scala
@@ -17,7 +17,7 @@ object CacherTests extends TestSuite{
}
object Middle extends Middle
trait Middle extends Base{
- def value = T{ super.value() + 2}
+ override def value = T{ super.value() + 2}
def overriden = T{ super.value()}
}
object Terminal extends Terminal
diff --git a/twirllib/src/mill/twirllib/TwirlModule.scala b/twirllib/src/mill/twirllib/TwirlModule.scala
new file mode 100644
index 00000000..2df70a1f
--- /dev/null
+++ b/twirllib/src/mill/twirllib/TwirlModule.scala
@@ -0,0 +1,56 @@
+package mill
+package twirllib
+
+import coursier.{Cache, MavenRepository}
+import mill.define.Sources
+import mill.eval.PathRef
+import mill.scalalib.Lib.resolveDependencies
+import mill.scalalib._
+import mill.util.Loose
+
+import scala.io.Codec
+import scala.util.Properties
+
+trait TwirlModule extends mill.Module {
+
+ def twirlVersion: T[String]
+
+ def twirlSources: Sources = T.sources {
+ millSourcePath / 'views
+ }
+
+ def twirlClasspath: T[Loose.Agg[PathRef]] = T {
+ resolveDependencies(
+ Seq(
+ Cache.ivy2Local,
+ MavenRepository("https://repo1.maven.org/maven2")
+ ),
+ Lib.depToDependency(_, "2.12.4"),
+ Seq(
+ ivy"com.typesafe.play::twirl-compiler:${twirlVersion()}",
+ ivy"org.scala-lang.modules::scala-parser-combinators:1.1.0"
+ )
+ )
+ }
+
+ // REMIND currently it's not possible to override these default settings
+ private def twirlAdditionalImports: Seq[String] = Nil
+
+ private def twirlConstructorAnnotations: Seq[String] = Nil
+
+ private def twirlCodec: Codec = Codec(Properties.sourceEncoding)
+
+ private def twirlInclusiveDot: Boolean = false
+
+ def compileTwirl: T[CompilationResult] = T.persistent {
+ TwirlWorkerApi.twirlWorker
+ .compile(
+ twirlClasspath().map(_.path),
+ twirlSources().map(_.path),
+ T.ctx().dest,
+ twirlAdditionalImports,
+ twirlConstructorAnnotations,
+ twirlCodec,
+ twirlInclusiveDot)
+ }
+}
diff --git a/twirllib/src/mill/twirllib/TwirlWorker.scala b/twirllib/src/mill/twirllib/TwirlWorker.scala
new file mode 100644
index 00000000..f351ff2f
--- /dev/null
+++ b/twirllib/src/mill/twirllib/TwirlWorker.scala
@@ -0,0 +1,119 @@
+package mill
+package twirllib
+
+import java.io.File
+import java.lang.reflect.Method
+import java.net.URLClassLoader
+
+import ammonite.ops.{Path, ls}
+import mill.eval.PathRef
+import mill.scalalib.CompilationResult
+
+import scala.io.Codec
+
+class TwirlWorker {
+
+ private var twirlInstanceCache = Option.empty[(Long, TwirlWorkerApi)]
+
+ private def twirl(twirlClasspath: Agg[Path]) = {
+ val classloaderSig = twirlClasspath.map(p => p.toString().hashCode + p.mtime.toMillis).sum
+ twirlInstanceCache match {
+ case Some((sig, instance)) if sig == classloaderSig => instance
+ case _ =>
+ val cl = new URLClassLoader(twirlClasspath.map(_.toIO.toURI.toURL).toArray)
+ val twirlCompilerClass = cl.loadClass("play.twirl.compiler.TwirlCompiler")
+ val compileMethod = twirlCompilerClass.getMethod("compile",
+ classOf[java.io.File],
+ classOf[java.io.File],
+ classOf[java.io.File],
+ classOf[java.lang.String],
+ cl.loadClass("scala.collection.Seq"),
+ cl.loadClass("scala.collection.Seq"),
+ cl.loadClass("scala.io.Codec"),
+ classOf[Boolean])
+
+ val defaultAdditionalImportsMethod = twirlCompilerClass.getMethod("compile$default$5")
+ val defaultConstructorAnnotationsMethod = twirlCompilerClass.getMethod("compile$default$6")
+ val defaultCodecMethod = twirlCompilerClass.getMethod("compile$default$7")
+ val defaultFlagMethod = twirlCompilerClass.getMethod("compile$default$8")
+
+ val instance = new TwirlWorkerApi {
+ override def compileTwirl(source: File,
+ sourceDirectory: File,
+ generatedDirectory: File,
+ formatterType: String,
+ additionalImports: Seq[String],
+ constructorAnnotations: Seq[String],
+ codec: Codec,
+ inclusiveDot: Boolean) {
+ val o = compileMethod.invoke(null, source,
+ sourceDirectory,
+ generatedDirectory,
+ formatterType,
+ defaultAdditionalImportsMethod.invoke(null),
+ defaultConstructorAnnotationsMethod.invoke(null),
+ defaultCodecMethod.invoke(null),
+ defaultFlagMethod.invoke(null))
+ }
+ }
+ twirlInstanceCache = Some((classloaderSig, instance))
+ instance
+ }
+ }
+
+ def compile(twirlClasspath: Agg[Path],
+ sourceDirectories: Seq[Path],
+ dest: Path,
+ additionalImports: Seq[String],
+ constructorAnnotations: Seq[String],
+ codec: Codec,
+ inclusiveDot: Boolean)
+ (implicit ctx: mill.util.Ctx): mill.eval.Result[CompilationResult] = {
+ val compiler = twirl(twirlClasspath)
+
+ def compileTwirlDir(inputDir: Path) {
+ ls.rec(inputDir).filter(_.name.matches(".*.scala.(html|xml|js|txt)"))
+ .foreach { template =>
+ val extFormat = twirlExtensionFormat(template.name)
+ compiler.compileTwirl(template.toIO,
+ inputDir.toIO,
+ dest.toIO,
+ s"play.twirl.api.$extFormat",
+ additionalImports,
+ constructorAnnotations,
+ codec,
+ inclusiveDot
+ )
+ }
+ }
+
+ sourceDirectories.foreach(compileTwirlDir)
+
+ val zincFile = ctx.dest / 'zinc
+ val classesDir = ctx.dest / 'html
+
+ mill.eval.Result.Success(CompilationResult(zincFile, PathRef(classesDir)))
+ }
+
+ private def twirlExtensionFormat(name: String) =
+ if (name.endsWith("html")) "HtmlFormat"
+ else if (name.endsWith("xml")) "XmlFormat"
+ else if (name.endsWith("js")) "JavaScriptFormat"
+ else "TxtFormat"
+}
+
+trait TwirlWorkerApi {
+ def compileTwirl(source: File,
+ sourceDirectory: File,
+ generatedDirectory: File,
+ formatterType: String,
+ additionalImports: Seq[String],
+ constructorAnnotations: Seq[String],
+ codec: Codec,
+ inclusiveDot: Boolean)
+}
+
+object TwirlWorkerApi {
+
+ def twirlWorker = new TwirlWorker()
+}
diff --git a/twirllib/test/resources/hello-world/core/views/hello.scala.html b/twirllib/test/resources/hello-world/core/views/hello.scala.html
new file mode 100644
index 00000000..acadf615
--- /dev/null
+++ b/twirllib/test/resources/hello-world/core/views/hello.scala.html
@@ -0,0 +1,6 @@
+@(title: String)
+<html>
+ <body>
+ <h1>@title</h1>
+ </body>
+</html> \ No newline at end of file
diff --git a/twirllib/test/src/mill/twirllib/HelloWorldTests.scala b/twirllib/test/src/mill/twirllib/HelloWorldTests.scala
new file mode 100644
index 00000000..31928335
--- /dev/null
+++ b/twirllib/test/src/mill/twirllib/HelloWorldTests.scala
@@ -0,0 +1,75 @@
+package mill.twirllib
+
+import ammonite.ops.{Path, cp, ls, mkdir, pwd, rm, _}
+import mill.util.{TestEvaluator, TestUtil}
+import utest.framework.TestPath
+import utest.{TestSuite, Tests, assert, _}
+
+object HelloWorldTests extends TestSuite {
+
+ trait HelloBase extends TestUtil.BaseModule {
+ override def millSourcePath: Path = TestUtil.getSrcPathBase() / millOuterCtx.enclosing.split('.')
+ }
+
+ trait HelloWorldModule extends mill.twirllib.TwirlModule {
+ def twirlVersion = "1.0.0"
+ }
+
+ object HelloWorld extends HelloBase {
+
+ object core extends HelloWorldModule {
+ override def twirlVersion = "1.3.15"
+ }
+ }
+
+ val resourcePath: Path = pwd / 'twirllib / 'test / 'resources / "hello-world"
+
+ def workspaceTest[T, M <: TestUtil.BaseModule](m: M, resourcePath: Path = resourcePath)
+ (t: TestEvaluator[M] => T)
+ (implicit tp: TestPath): T = {
+ val eval = new TestEvaluator(m)
+ rm(m.millSourcePath)
+ rm(eval.outPath)
+ mkdir(m.millSourcePath / up)
+ cp(resourcePath, m.millSourcePath)
+ t(eval)
+ }
+
+ def compileClassfiles: Seq[RelPath] = Seq[RelPath](
+ "hello.template.scala"
+ )
+
+ def tests: Tests = Tests {
+ 'twirlVersion - {
+
+ 'fromBuild - workspaceTest(HelloWorld) { eval =>
+ val Right((result, evalCount)) = eval.apply(HelloWorld.core.twirlVersion)
+
+ assert(
+ result == "1.3.15",
+ evalCount > 0
+ )
+ }
+ }
+ 'compileTwirl - workspaceTest(HelloWorld) { eval =>
+ val Right((result, evalCount)) = eval.apply(HelloWorld.core.compileTwirl)
+
+ val outputFiles = ls.rec(result.classes.path)
+ val expectedClassfiles = compileClassfiles.map(
+ eval.outPath / 'core / 'compileTwirl / 'dest / 'html / _
+ )
+ assert(
+ result.classes.path == eval.outPath / 'core / 'compileTwirl / 'dest / 'html,
+ outputFiles.nonEmpty,
+ outputFiles.forall(expectedClassfiles.contains),
+ outputFiles.size == 1,
+ evalCount > 0
+ )
+
+ // don't recompile if nothing changed
+ val Right((_, unchangedEvalCount)) = eval.apply(HelloWorld.core.compileTwirl)
+
+ assert(unchangedEvalCount == 0)
+ }
+ }
+}