SBT build definition

Since Scala.js is quite new and it's been evolving even rather recently, building Scala.js applications with SBT is not as clear as it could be. Yes, the documentation and tutorials give you the basics, but what if you want something more, like configure a custom directory layout?

The build.sbt in this tutorial shows you some typical cases you might run into in your own application. The basic structure of the build.sbt is built on top of the example provided by Vincent Munier, author of the sbt-play-scalajs plugin.

The build defines three separate projects:

  • shared
  • client
  • server

Shared project

First one is a special Scala.js CrossProject that actually contains two projects: one for JS and one for JVM. This shared project contains classes, libraries and resources shared between the client and server. In the context of this tutorial it means just the Api.scala trait and TodoItem.scala case class. In a more realistic application you would have your data models etc. defined here.

lazy val shared = (crossProject.crossType(CrossType.Pure) in file("shared"))
  .settings(
    scalaVersion := Settings.versions.scala,
    libraryDependencies ++= Settings.sharedDependencies.value
  )
  // set up settings specific to the JS project
  .jsConfigure(_ enablePlugins ScalaJSPlay)
  .jsSettings(sourceMapsBase := baseDirectory.value / "..")

The shared dependencies include libraries used by both client and server such as autowire and boopickle for client/server communication.

val sharedDependencies = Def.setting(Seq(
  "com.lihaoyi" %%% "autowire" % versions.autowire,
  "me.chrons" %%% "boopickle" % versions.booPickle,
  "com.lihaoyi" %%% "utest" % versions.uTest
))

Client project

Client is defined as a normal Scala.js project by enabling the ScalaJSPlugin on it.

lazy val client: Project = (project in file("client"))
  .settings(
    name := "client",
    version := Settings.version,
    scalaVersion := Settings.versions.scala,
    scalacOptions ++= Settings.scalacOptions,
    libraryDependencies ++= Settings.scalajsDependencies.value,
    // by default we do development build, no eliding
    elideOptions := Seq(),
    scalacOptions ++= elideOptions.value,
    jsDependencies ++= Settings.jsDependencies.value,
    // RuntimeDOM is needed for tests
    jsDependencies += RuntimeDOM % "test",
    // yes, we want to package JS dependencies
    skip in packageJSDependencies := false,
    // use Scala.js provided launcher code to start the client app
    persistLauncher := true,
    persistLauncher in Test := false,
    // must specify source maps location because we use pure CrossProject
    sourceMapsDirectories += sharedJS.base / "..",
    // use uTest framework for tests
    testFrameworks += new TestFramework("utest.runner.Framework")
  )
  .enablePlugins(ScalaJSPlugin, ScalaJSPlay)
  .dependsOn(sharedJS)

First few settings are normal Scala settings, but let's go through the remaining settings to explain what they do.

    // by default we do development build, no eliding
    elideOptions := Seq(),
    scalacOptions ++= elideOptions.value,

Eliding is used to remove code that is not needed in the production build, such as debug logging. This setting is empty by default, but is enabled in the release command.

    jsDependencies ++= Settings.jsDependencies.value,
    // RuntimeDOM is needed for tests
    jsDependencies += RuntimeDOM % "test",
    // yes, we want to package JS dependencies
    skip in packageJSDependencies := false,

The jsDependencies defines a set of JavaScript libraries your application depends on. These are also packaged into a single .js file for easy consumptions. For test phase we include the RuntimeDOM so that Scala.js plugin knows to use PhantomJS instead of the default Rhino to run the tests. Make sure you have installed PhantomJS before running the tests.

    // use Scala.js provided launcher code to start the client app
    persistLauncher := true,
    persistLauncher in Test := false,

This setting informs Scala.js plugin to generate a special launcher.js file, which is loaded last and invokes your main method. Using a launcher keeps your HTML template clean, as you don't need to specify the main function there.

    // must specify source maps location because we use pure CrossProject
    sourceMapsDirectories += sharedJS.base / "..",

Because we are using a pure CrossProject, the source map directories have to be manually adjusted to reflect where the source files can be found.

    // use uTest framework for tests
    testFrameworks += new TestFramework("utest.runner.Framework")

Lets SBT know that we are using uTest framework for tests.

  .enablePlugins(ScalaJSPlugin, ScalaJSPlay)
  .dependsOn(sharedJS)

We enable both Scala.js and Scala.js-for-Play plugins. Finally the client project needs to depend on the shared project to get access to shared code and resources.

Server project

The server project is a normal Play project with a few twists to make client integration a breeze. Most of the heavy-lifting is done by the ScalaJSPlay plugin, which is automatically included to all projects using PlayScala plugin.

lazy val server = (project in file("server"))
  .settings(
    name := "server",
    version := Settings.version,
    scalaVersion := Settings.versions.scala,
    scalacOptions ++= Settings.scalacOptions,
    libraryDependencies ++= Settings.jvmDependencies.value,
    commands += ReleaseCmd,
    // connect to the client project
    scalaJSProjects := clients,
    pipelineStages := Seq(scalaJSProd),
    // compress CSS
    LessKeys.compress in Assets := true
  )
  .enablePlugins(PlayScala)
  .disablePlugins(PlayLayoutPlugin) // use the standard directory layout instead of Play's custom
  .aggregate(clients.map(projectToRef): _*)
  .dependsOn(sharedJVM)

As with the client project, the first few settings are just normal SBT settings, so let's focus on the more interesting ones.

    commands += ReleaseCmd,

We define a new SBT command release to run a sequence of commands to produce a distribution package.

    // connect to the client project
    scalaJSProjects := clients,
    pipelineStages := Seq(scalaJSProd),

Let the plugin know where our client project is and enable Scala.js processing in the pipeline.

    // compress CSS
    LessKeys.compress in Assets := true,

This instructs the sbt-less plugin to minify the produced CSS.

  .enablePlugins(PlayScala)
  .disablePlugins(PlayLayoutPlugin) // use the standard directory layout instead of Play's custom

We use Play, but not its default layout. Instead we prefer the normal SBT layout with src/main/scala structure.

  .aggregate(clients.map(projectToRef): _*)
  .dependsOn(sharedJVM)

Server aggregates the client and also depends on the shared project to get access to shared code and resources.

results matching ""

    No results matching ""