Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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)
- Loading branch information
1 parent
ab2a8b3
commit 02436b4
Showing
8 changed files
with
265 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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() | ||
} |
6 changes: 6 additions & 0 deletions
6
twirllib/test/resources/hello-world/core/views/hello.scala.html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
@(title: String) | ||
<html> | ||
<body> | ||
<h1>@title</h1> | ||
</body> | ||
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
} | ||
} | ||
} |