Cross-Building

It is often desirable to compile the same source code with Scala.js and Scala JVM. In order to do this, you need two different projects, one for Scala.js and one for Scala JVM and a folder with the shared source code. You can then tell sbt to use the shared source folder in addition to the normal source locations.

To do this, there is a separate sbt plugin, sbt-crossproject, which provides a builder crossProject constructing two related sbt projects, one for the JVM, and one for JS. The readme of sbt-crossproject provides examples and documentation.

We give here a simple example of how such a project could look like. You can find this project on GitHub.

Directory Structure

<project root>
 +- jvm
 |   +- src/main/scala
 +- js
 |   +- src/main/scala
 +- shared
     +- src/main/scala

In shared/src/main/scala are the shared source files. In {js|jvm}/src/main/scala are the source files specific to the respective platform (these folders are optional).

sbt Build File

First, you need to add sbt-scalajs-crossproject in your project/plugins.sbt file:

addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.17.0")
addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.3.2")

You can then use the crossProject builder in your build.sbt file:

ThisBuild / scalaVersion := "2.13.14"

lazy val root = project.in(file(".")).
  aggregate(foo.js, foo.jvm).
  settings(
    publish := {},
    publishLocal := {},
  )

lazy val foo = crossProject(JSPlatform, JVMPlatform).in(file(".")).
  settings(
    name := "foo",
    version := "0.1-SNAPSHOT",
  ).
  jvmSettings(
    // Add JVM-specific settings here
  ).
  jsSettings(
    // Add JS-specific settings here
    scalaJSUseMainModuleInitializer := true,
  )

Note that enablePlugins(ScalaJSPlugin) must not be included when using crossProject.

You now have separate projects to compile towards Scala.js and Scala JVM. Note the same name given to both projects, this allows them to be published with corresponding artifact names:

  • foo_2.13-0.1-SNAPSHOT.jar
  • foo_sjs1_2.13-0.1-SNAPSHOT.jar

If you do not publish the artifacts, you may choose different names for the projects.

Dependencies

If your cross compiled source depends on libraries, you may use %%% for both projects. It will automatically determine whether you are in a Scala/JVM or a Scala.js project. For example, if your code uses Scalatags, your project definitions look like this:

lazy val foo = crossProject.in(file("."))
  .settings(
    // other settings
    libraryDependencies += "com.lihaoyi" %%% "scalatags" % "0.8.5",
  )

instead of the more repetitive variant:

lazy val foo = crossProject.in(file("."))
  .settings(
    // other settings
  )
  .jvmSettings(
    libraryDependencies += "com.lihaoyi" %% "scalatags" % "0.8.5",
  )
  .jsSettings(
    libraryDependencies += "com.lihaoyi" %%% "scalatags" % "0.8.5",
  )

Exporting shared classes to JavaScript

When working with shared classes, you may want to export some of them to JavaScript. This is done with annotations as explained in Export Scala.js APIs to JavaScript.

These annotations are part of the Scala.js library which is not available on the Scala JVM project. In order for the annotated classes to compile on the JVM project, you should add the scalajs-stubs library to your JVM dependencies as “provided” (used only during compilation and not included at runtime):

  .jvmSettings(
    ...
    libraryDependencies += "org.scala-js" %% "scalajs-stubs" % "1.1.0" % "provided",
  )

scalajs-stubs is a tiny JVM (only) library containing Scala.js export annotations.