Skip to content

Commit

Permalink
Add a TwirlModule to compile Twirl templates (#271)
Browse files Browse the repository at this point in the history
* 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)
  • Loading branch information
ggrossetie authored and rockjam committed May 24, 2018
1 parent ab2a8b3 commit 02436b4
Show file tree
Hide file tree
Showing 8 changed files with 265 additions and 3 deletions.
6 changes: 6 additions & 0 deletions build.sc
Expand Up @@ -196,6 +196,12 @@ object scalajslib extends MillModule {
}
}

object twirllib extends MillModule {

def moduleDeps = Seq(scalalib)

}

def testRepos = T{
Seq(
"MILL_ACYCLIC_REPO" ->
Expand Down
2 changes: 1 addition & 1 deletion ci/test-mill-0.sh
Expand Up @@ -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
2 changes: 1 addition & 1 deletion ci/test-mill-dev.sh
Expand Up @@ -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

2 changes: 1 addition & 1 deletion main/test/src/mill/define/CacherTests.scala
Expand Up @@ -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
Expand Down
56 changes: 56 additions & 0 deletions 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)
}
}
119 changes: 119 additions & 0 deletions 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()
}
@@ -0,0 +1,6 @@
@(title: String)
<html>
<body>
<h1>@title</h1>
</body>
</html>
75 changes: 75 additions & 0 deletions 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)
}
}
}

0 comments on commit 02436b4

Please sign in to comment.