Scala Web Build Examples
This page contains examples of using Mill as a build tool for web-applications. It covers setting up a basic backend server, Todo-MVC app, topics like cache busting, as well as usage of Scala.js both as standalone modules as well as integrated with your backend Scala-JVM web server.
TodoMVC Web App
import mill._, scalalib._
object root extends RootModule with ScalaModule {
def scalaVersion = "2.13.8"
def ivyDeps = Agg(
ivy"com.lihaoyi::cask:0.9.1",
ivy"com.lihaoyi::scalatags:0.12.0"
)
object test extends ScalaTests {
def testFramework = "utest.runner.Framework"
def ivyDeps = Agg(
ivy"com.lihaoyi::utest::0.8.4",
ivy"com.lihaoyi::requests::0.6.9",
)
}
}
This example demonstrates how to set up a simple Scala webserver implementing the popular Todo-MVC demo application. It includes a test suite that spins up the web server locally and makes HTTP requests against it.
> ./mill test
+ webapp.WebAppTests.simpleRequest...
> ./mill runBackground
> curl http://localhost:8080
...What needs to be done...
...
> ./mill clean runBackground
Webapp Cache Busting
import mill._, scalalib._
import java.util.Arrays
object root extends RootModule with ScalaModule {
def scalaVersion = "2.13.8"
def ivyDeps = Agg(
ivy"com.lihaoyi::cask:0.9.1",
ivy"com.lihaoyi::scalatags:0.12.0",
ivy"com.lihaoyi::os-lib:0.9.1"
)
def resources = T {
val hashMapping = for {
resourceRoot <- super.resources()
path <- os.walk(resourceRoot.path)
if os.isFile(path)
} yield hashFile(path, resourceRoot.path, T.dest)
os.write(
T.dest / "hashed-resource-mapping.json",
upickle.default.write(hashMapping.toMap, indent = 4)
)
Seq(PathRef(T.dest))
}
object test extends ScalaTests {
def testFramework = "utest.runner.Framework"
def ivyDeps = Agg(
ivy"com.lihaoyi::utest::0.8.4",
ivy"com.lihaoyi::requests::0.6.9",
)
}
}
def hashFile(path: os.Path, src: os.Path, dest: os.Path) = {
val hash = Integer.toHexString(Arrays.hashCode(os.read.bytes(path)))
val relPath = path.relativeTo(src)
val ext = if (relPath.ext == "") "" else s".${relPath.ext}"
val hashedPath = relPath / os.up / s"${relPath.baseName}-$hash$ext"
os.copy(path, dest / hashedPath, createFolders = true)
(relPath.toString(), hashedPath.toString())
}
This example demonstrates how to implement webapp "cache busting" in Mill, where we serve static files with a hash appended to the filename, and save a mapping of filename to hashed filename so that the web server can serve HTML that references the appropriately hashed file paths. This allows us to deploy the static files behind caches with long expiration times, while still having the web app immediately load updated static files after a deploy (since the HTML will reference new hashed paths that are not yet in the cache).
We do this in an overrride of the resources
target, that loads
super.resources()
, hashes the files within it using Arrays.hashCode
, and
copies the files to a new hashed path saving the overall mapping to a
hashed-resource-mapping.json
. The webapp then loads the mapping at runtime
and uses it to serve HTML referencing the hashed paths, but without paying
the cost of hashing the static resource files at runtime.
> ./mill test
+ webapp.WebAppTests.simpleRequest ...
> ./mill runBackground
> curl http://localhost:8081
...What needs to be done...
...
> curl http://localhost:8081/static/main-6da98e99.js # mac/linux
initListeners()
> ./mill clean runBackground
Scala.js Modules
import mill._, scalalib._, scalajslib._
object foo extends ScalaJSModule {
def scalaVersion = "2.13.14"
def scalaJSVersion = "1.16.0"
def ivyDeps = Agg(ivy"com.lihaoyi::scalatags::0.12.0")
object test extends ScalaJSTests {
def ivyDeps = Agg(ivy"com.lihaoyi::utest::0.8.4")
def testFramework = "utest.runner.Framework"
}
}
This build defines a single ScalaJSModule
with a test suite.
ScalaJSModule
is similar to ScalaModule
, except it requires a
scalaJSVersion
to be provided.
By default, Scala.js code gets access to the scala.scalajs.js
package,
which contains the core JS APIs like js.JSON
, js.Date
, etc. ivyDeps
of
Scala.js-compatible dependencies can be given, which need two colons (::
)
on the right to indicate it’s a Scala.js dependency. These can be both Scala
libraries compiled to JS, or facades for Javascript libraries. If running in
the browser, you can use the https://github.com/scala-js/scala-js-dom facade
to access the browser DOM APIs.
Normal tasks like compile
, run
, or test
work as expected, with run
and test
calling node
to run in a Javascript environment rather than in
the JVM. There is also additional fastLinkJS
and fullLinkJS
commands
that compile the module into a single Javascript file, which you can then
distribute or deploy with your web application
> ./mill foo.run
<h1>Hello World</h1>
stringifiedJsObject: ["hello","world","!"]
> ./mill foo.test
+ foo.FooTests.hello...
> ./mill show foo.fullLinkJS # mac/linux
{
...
..."jsFileName": "main.js",
"dest": ".../out/foo/fullLinkJS.dest"
}
> node out/foo/fullLinkJS.dest/main.js # mac/linux
<h1>Hello World</h1>
stringifiedJsObject: ["hello","world","!"]
Note that running Scala.js modules locally requires the node
Javascript
runtime to be installed on your machine.
Scala.js Webserver Integration
import mill._, scalalib._, scalajslib._
object root extends RootModule with ScalaModule {
def scalaVersion = "2.13.14"
def ivyDeps = Agg(
ivy"com.lihaoyi::cask:0.9.1",
ivy"com.lihaoyi::scalatags:0.12.0"
)
def resources = T {
os.makeDir(T.dest / "webapp")
val jsPath = client.fastLinkJS().dest.path
// Move main.js[.map]into the proper filesystem position
// in the resource folder for the web server code to pick up
os.copy(jsPath / "main.js", T.dest / "webapp" / "main.js")
os.copy(jsPath / "main.js.map", T.dest / "webapp" / "main.js.map")
super.resources() ++ Seq(PathRef(T.dest))
}
object test extends ScalaTests {
def testFramework = "utest.runner.Framework"
def ivyDeps = Agg(
ivy"com.lihaoyi::utest::0.8.4",
ivy"com.lihaoyi::requests::0.6.9",
)
}
object client extends ScalaJSModule {
def scalaVersion = "2.13.14"
def scalaJSVersion = "1.16.0"
def ivyDeps = Agg(ivy"org.scala-js::scalajs-dom::2.2.0")
}
}
A minimal example of a Scala-JVM backend server wired up with a Scala.js
front-end. The backend code is identical to the TodoMVC Web App example, but
we replace the main.js
client side code with the Javascript output of
ClientApp.scala
.
Note that the client-side Scala code is the simplest 1-to-1 translation of
the original Javascript, using scalajs-dom
, as this example is intended to
demonstrate the build.sc
config in Mill. A real codebase is likely to use
Javascript or Scala UI frameworks to manage the UI, but those are beyond the
scope of this example.
> ./mill test
+ webapp.WebAppTests.simpleRequest ...
> ./mill runBackground
> curl http://localhost:8082
...What needs to be done...
...
> curl http://localhost:8082/static/main.js
..."org.scalajs.linker.runtime.RuntimeLong"...
...
> ./mill clean runBackground
Scala.js/Scala-JVM Code Sharing
import mill._, scalalib._, scalajslib._
trait AppScalaModule extends ScalaModule {
def scalaVersion = "3.3.3"
}
trait AppScalaJSModule extends AppScalaModule with ScalaJSModule {
def scalaJSVersion = "1.16.0"
}
object root extends RootModule with AppScalaModule {
def moduleDeps = Seq(shared.jvm)
def ivyDeps = Agg(ivy"com.lihaoyi::cask:0.9.1")
def resources = T{
os.makeDir(T.dest / "webapp")
val jsPath = client.fastLinkJS().dest.path
os.copy(jsPath / "main.js", T.dest / "webapp" / "main.js")
os.copy(jsPath / "main.js.map", T.dest / "webapp" / "main.js.map")
super.resources() ++ Seq(PathRef(T.dest))
}
object test extends ScalaTests with TestModule.Utest {
def ivyDeps = Agg(
ivy"com.lihaoyi::utest::0.8.4",
ivy"com.lihaoyi::requests::0.6.9",
)
}
object shared extends Module {
trait SharedModule extends AppScalaModule with PlatformScalaModule {
def ivyDeps = Agg(
ivy"com.lihaoyi::scalatags::0.12.0",
ivy"com.lihaoyi::upickle::3.0.0",
)
}
object jvm extends SharedModule
object js extends SharedModule with AppScalaJSModule
}
object client extends AppScalaJSModule {
def moduleDeps = Seq(shared.js)
def ivyDeps = Agg(ivy"org.scala-js::scalajs-dom::2.2.0")
}
}
A Scala-JVM backend server wired up with a Scala.js front-end, with a
shared
module containing code that is used in both client and server.
Rather than the server sending HTML for the initial page load and HTML for
page updates, it sends HTML for the initial load and JSON for page updates
which is then rendered into HTML on the client.
The JSON serialization logic and HTML generation logic in the shared
module
is shared between client and server, and uses libraries like uPickle and
Scalatags which work on both ScalaJVM and Scala.js. This allows us to freely
move code between the client and server, without worrying about what
platform or language the code was originally implemented in.
This is a minimal example of shared code compiled to ScalaJVM and Scala.js, running on both client and server, meant for illustrating the build configuration. A full exploration of client-server code sharing techniques is beyond the scope of this example.
> ./mill test
+ webapp.WebAppTests.simpleRequest ...
> ./mill runBackground
> curl http://localhost:8083
...What needs to be done...
...
> curl http://localhost:8083/static/main.js
...Scala.js...
...
> ./mill clean runBackground
Publishing Cross-Platform Scala Modules
import mill._, scalalib._, scalajslib._, publish._
object foo extends Cross[FooModule]("2.13.14", "3.3.3")
trait FooModule extends Cross.Module[String] {
trait Shared extends CrossScalaModule with CrossValue with PlatformScalaModule with PublishModule {
def publishVersion = "0.0.1"
def pomSettings = PomSettings(
description = "Hello",
organization = "com.lihaoyi",
url = "https://github.com/lihaoyi/example",
licenses = Seq(License.MIT),
versionControl = VersionControl.github("lihaoyi", "example"),
developers = Seq(Developer("lihaoyi", "Li Haoyi", "https://github.com/lihaoyi"))
)
def ivyDeps = Agg(ivy"com.lihaoyi::scalatags::0.12.0")
}
trait FooTestModule extends TestModule {
def ivyDeps = Agg(ivy"com.lihaoyi::utest::0.8.4")
def testFramework = "utest.runner.Framework"
}
trait SharedJS extends Shared with ScalaJSModule {
def scalaJSVersion = "1.16.0"
}
object bar extends Module {
object jvm extends Shared{
object test extends ScalaTests with FooTestModule
}
object js extends SharedJS {
object test extends ScalaJSTests with FooTestModule
}
}
object qux extends Module {
object jvm extends Shared {
def moduleDeps = Seq(bar.jvm)
def ivyDeps = super.ivyDeps() ++ Agg(ivy"com.lihaoyi::upickle::3.0.0")
object test extends ScalaTests with FooTestModule
}
object js extends SharedJS {
def moduleDeps = Seq(bar.js)
object test extends ScalaJSTests with FooTestModule
}
}
}
This example demonstrates how to publish Scala modules which are both cross-version and cross-platform: running on both Scala 2.13.14/3.3.3 as well as Scala-JVM/JS.
> ./mill show foo[2.13.14].bar.jvm.sources
[
".../foo/bar/src",
".../foo/bar/src-jvm",
".../foo/bar/src-2.13.14",
".../foo/bar/src-2.13.14-jvm",
".../foo/bar/src-2.13",
".../foo/bar/src-2.13-jvm",
".../foo/bar/src-2",
".../foo/bar/src-2-jvm"
]
> ./mill show foo[3.3.3].qux.js.sources
[
".../foo/qux/src",
".../foo/qux/src-js",
".../foo/qux/src-3.3.3",
".../foo/qux/src-3.3.3-js",
".../foo/qux/src-3.3",
".../foo/qux/src-3.3-js",
".../foo/qux/src-3",
".../foo/qux/src-3-js"
]
> ./mill foo[2.13.14].qux.jvm.run
Bar.value: <p>world Specific code for Scala 2.x</p>
Parsing JSON with ujson.read
Qux.main: Set(<p>i</p>, <p>cow</p>, <p>me</p>)
> ./mill foo[3.3.3].qux.js.run
Bar.value: <p>world Specific code for Scala 3.x</p>
Parsing JSON with js.JSON.parse
Qux.main: Set(<p>i</p>, <p>cow</p>, <p>me</p>)
> ./mill foo[3.3.3].__.js.test
+ bar.BarTests.test ... <p>world Specific code for Scala 3.x</p>
+ qux.QuxTests.parseJsonGetKeys ... Set(i, cow, me)
> ./mill __.publishLocal
Publishing Artifact(com.lihaoyi,foo-bar_sjs1_2.13,0.0.1) to ivy repo...
Publishing Artifact(com.lihaoyi,foo-bar_2.13,0.0.1) to ivy repo...
Publishing Artifact(com.lihaoyi,foo-qux_sjs1_2.13,0.0.1) to ivy repo...
Publishing Artifact(com.lihaoyi,foo-qux_2.13,0.0.1) to ivy repo...
Publishing Artifact(com.lihaoyi,foo-bar_sjs1_3,0.0.1) to ivy repo...
Publishing Artifact(com.lihaoyi,foo-bar_3,0.0.1) to ivy repo...
Publishing Artifact(com.lihaoyi,foo-qux_sjs1_3,0.0.1) to ivy repo...
Publishing Artifact(com.lihaoyi,foo-qux_3,0.0.1) to ivy repo...
Publishing Cross-Platform Scala Modules Alternative
import mill._, scalalib._, scalajslib._, publish._
trait Shared extends CrossScalaModule with PlatformScalaModule with PublishModule {
def publishVersion = "0.0.1"
def pomSettings = PomSettings(
description = "Hello",
organization = "com.lihaoyi",
url = "https://github.com/lihaoyi/example",
licenses = Seq(License.MIT),
versionControl = VersionControl.github("lihaoyi", "example"),
developers = Seq(Developer("lihaoyi", "Li Haoyi", "https://github.com/lihaoyi"))
)
def ivyDeps = Agg(ivy"com.lihaoyi::scalatags::0.12.0")
}
trait SharedTestModule extends TestModule {
def ivyDeps = Agg(ivy"com.lihaoyi::utest::0.8.4")
def testFramework = "utest.runner.Framework"
}
trait SharedJS extends Shared with ScalaJSModule {
def scalaJSVersion = "1.16.0"
}
val scalaVersions = Seq("2.13.14", "3.3.3")
object bar extends Module {
object jvm extends Cross[JvmModule](scalaVersions)
trait JvmModule extends Shared {
object test extends ScalaTests with SharedTestModule
}
object js extends Cross[JsModule](scalaVersions)
trait JsModule extends SharedJS {
object test extends ScalaJSTests with SharedTestModule
}
}
object qux extends Module {
object jvm extends Cross[JvmModule](scalaVersions)
trait JvmModule extends Shared {
def moduleDeps = Seq(bar.jvm())
def ivyDeps = super.ivyDeps() ++ Agg(ivy"com.lihaoyi::upickle::3.0.0")
object test extends ScalaTests with SharedTestModule
}
object js extends Cross[JsModule](scalaVersions)
trait JsModule extends SharedJS {
def moduleDeps = Seq(bar.js())
object test extends ScalaJSTests with SharedTestModule
}
}
This example demonstrates an alternative way of defining your cross-platform
cross-version modules: rather than wrapping them all in a foo
cross-module to provide the different versions, we instead give each module
bar.jvm
, bar.js
, qux.jvm
, qux.js
its own Cross
module. This
approach can be useful if the different cross modules need to support
different sets of Scala versions, as it allows you to specify the
scalaVersions
passed to each individual cross module separately.
> ./mill show qux.js[3.3.3].sources
[
".../qux/src",
".../qux/src-js",
".../qux/src-3.3.3",
".../qux/src-3.3.3-js",
".../qux/src-3.3",
".../qux/src-3.3-js",
".../qux/src-3",
".../qux/src-3-js"
]
> ./mill show qux.js[3.3.3].test.sources
[
".../qux/test/src",
".../qux/test/src-js",
".../qux/test/src-3.3.3",
".../qux/test/src-3.3.3-js",
".../qux/test/src-3.3",
".../qux/test/src-3.3-js",
".../qux/test/src-3",
".../qux/test/src-3-js"
]
> ./mill qux.jvm[2.13.14].run
Bar.value: <p>world Specific code for Scala 2.x</p>
Parsing JSON with ujson.read
Qux.main: Set(<p>i</p>, <p>cow</p>, <p>me</p>)
> ./mill __.js[3.3.3].test
+ bar.BarTests.test ... <p>world Specific code for Scala 3.x</p>
+ qux.QuxTests.parseJsonGetKeys ... Set(i, cow, me)
> ./mill __.publishLocal
...
Publishing Artifact(com.lihaoyi,bar_sjs1_2.13,0.0.1) to ivy repo...
Publishing Artifact(com.lihaoyi,bar_2.13,0.0.1) to ivy repo...
Publishing Artifact(com.lihaoyi,qux_sjs1_2.13,0.0.1) to ivy repo...
Publishing Artifact(com.lihaoyi,qux_2.13,0.0.1) to ivy repo...
Publishing Artifact(com.lihaoyi,bar_sjs1_3,0.0.1) to ivy repo...
Publishing Artifact(com.lihaoyi,bar_3,0.0.1) to ivy repo...
Publishing Artifact(com.lihaoyi,qux_sjs1_3,0.0.1) to ivy repo...
Publishing Artifact(com.lihaoyi,qux_3,0.0.1) to ivy repo...