Announcing Scala.js 1.0.0

Feb 25, 2020.

We are thrilled to announce the General Availability release of Scala.js 1.0.0!

Scala.js is a close dialect of Scala compiling to JavaScript, featuring great portability wrt. Scala/JVM, interoperability with JavaScript, and performance.

After 7 years of development, including 5 years of stability within the 0.6.x series, we are finally ready to present Scala.js 1.0.0. Quoting Antoine de Saint-Exupéry,

Perfection is achieved, not when there is nothing more to add, but when there is nothing left to take away.

Scala.js 1.0.0 is the culmination of our efforts to simplify, specify, and future-proof all aspects of Scala.js, from the language semantics to the internal APIs, so that there is nothing left to take away.

As the change in major version number witnesses, this release is not binary compatible with 0.6.x, nor with the previous milestones and RCs of the 1.x series. Libraries need to be recompiled and republished using Scala.js 1.0.0 to be compatible. Several libraries at the core of the Scala/Scala.js ecosystem have already been published and are available for Scala.js 1.0.0. Here is a list of libraries known to support Scala.js 1.x.

Moreover, this release is not entirely source compatible with 0.6.x either (see Breaking Changes below).

Here are some highlights of Scala.js 1.0.0:

  • Better interoperability with JavaScript libraries (see the Enhancements section below)
  • Improved portability with respect to Scala/JVM
  • Better run-time performance

Getting started

If you are new to Scala.js, head over to the tutorial.

If you need help with anything related to Scala.js, you may find our community on Gitter and on Stack Overflow.

Bug reports can be filed on GitHub.

Preparations before upgrading from 0.6.x

Before upgrading to 1.0.0, we strongly recommend that you upgrade to Scala.js 0.6.32 or later, and address all deprecation warnings. Since Scala.js 1.0.0 removes support for all the deprecated features in 0.6.x, it is easier to see the deprecation messages guiding you to the proper replacements.

Moreover, you will need to upgrade your version of Scala to at least 2.11.12, 2.12.1 or 2.13.0 (the latest being 2.11.12, 2.12.10 and 2.13.1 as of this writing). Older versions (including all 2.10.x versions) are not supported anymore by Scala.js 1.x.

For sbt users

Make sure that you explicitly use sbt-crossproject instead of the default crossProject implementation. The old crossProject is deprecated in 0.6.32, but it is easy to overlook deprecations in the build itself.

Additionally to the explicitly deprecated things, make sure to use scalaJSLinkerConfig instead of the following sbt settings:

  • scalaJSSemantics
  • scalaJSModuleKind
  • scalaJSOutputMode
  • emitSourceMaps
  • relativeSourceMaps
  • scalaJSOptimizerOptions

Finally, if you are still using sbt 0.13.x, you will have to upgrade to sbt 1.2.1 or later (as of this writing, the latest release is 1.3.8).

Upgrade to 1.0.0 from 0.6.32 or later

These instructions apply to sbt users. If you are using a different build tool, refer to that build tool’s documentation for Scala.js support.

As a first approximation, all you need to do is to update the version number in project/plugins.sbt:

addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.0.0")

In addition, if you use some of the components that have been moved to separate repositories, you will need to add some more dependencies in project/plugins.sbt:

If you use scalajs-stubs:

  • Change its version number to "1.0.0"

If you use jsDependencies (or rely on the jsDependencies of your transitive dependencies):

  • Add addSbtPlugin("org.scala-js" % "sbt-jsdependencies" % "1.0.0") in project/plugins.sbt
  • Add .enablePlugins(JSDependenciesPlugin) to Scala.js projects
  • Add .jsConfigure(_.enablePlugins(JSDependenciesPlugin)) to crossProjects

If you use the Node.js with jsdom environment (JSDOMNodeJSEnv):

  • Add libraryDependencies += "org.scala-js" %% "scalajs-env-jsdom-nodejs" % "1.0.0" in project/plugins.sbt

If you use the PhantomJS environment (PhantomJSEnv):

  • Add addSbtPlugin("org.scala-js" % "sbt-scalajs-env-phantomjs" % "1.0.0") in project/plugins.sbt

If you use the Selenium environment (SeleniumJSEnv):

  • Change its version number to "1.0.0"

Note: All the above version numbers coincide at "1.0.0" for this release, but they will independently evolve in the future. Therefore, do not “DRY up” your build by using a common constant (such as scalaJSVersion) for those dependencies, or your build will break when updating one of them in the future.

Finally, if your build has

scalacOptions += "-P:scalajs:sjsDefinedByDefault"

you will need to remove it (Scala.js 1.x always behaves as if sjsDefinedByDefault were present).

This should get your build up to speed to Scala.js 1.0.0. From there, you should be able to test whether things go smoothly, or whether you are affected by the breaking changes detailed below.

Breaking changes

This section discusses the backward incompatible changes, which might affect your project.

Access to the global scope instead of the global object

This is the only major breaking change at the language level. In Scala.js 1.x, js.Dynamic.global and @JSGlobalScope objects refer to the global scope of JavaScript, rather than the global object. Concretely, this has three consequences, which we outline below. Further information can be found in the documentation about the global scope in Scala.js.

Members can only be accessed with a statically known name which is a valid JavaScript identifier

For example, the following is valid:

println(js.Dynamic.global.Math)

but the following variant, where the name Math is only known at run-time, is not valid anymore:

val mathName = "Math"
println(js.Dynamic.global.selectDynamic(mathName))

The latter will cause a compile error. This is because it is not possible to perform dynamic lookups in the global scope. Similarly, accessing a member whose name is statically known but not a valid JavaScript identifier is also prohibited:

println(js.Dynamic.global.`not-a-valid-JS-identifier`)

Global scope objects cannot be stored in a separate val

For example, the following is invalid and will cause a compile error:

val g = js.Dynamic.global

as well as:

def foo(x: Any): Unit = println(x)
foo(js.Dynamic.global)

This follows from the previous rule. If the above two snippets were allowed, we could not check that we only access members with statically known names.

The first snippet can be advantageously replaced by a renaming import:

import js.Dynamic.{global => g}

Accessing a member that is not declared causes a ReferenceError to be thrown

This is a run-time behavior change, and in our experience the larger source of breakages in actual code.

Previously, reading a non-existent member of the global object, such as

println(js.Dynamic.global.globalVarThatDoesNotExist)

would evaluate to undefined. In Scala.js 1.x, this throws a ReferenceError. Similarly, writing to a non-existent member, such as

js.Dynamic.global.globalVarThatDoesNotExist = 42

would previously create said global variable. In Scala.js 1.x, it also throws a ReferenceError.

A typical use case of the previous behavior was to test whether a global variable was defined or not, e.g.,

if (js.isUndefined(js.Dynamic.global.Promise)) {
  // Promises are not supported
} else {
  // Promises are supported
}

This idiom is broken in Scala.js 1.x, and needs to be replaced by an explicit use of js.typeOf:

if (js.typeOf(js.Dynamic.global.Promise) == "undefined")

The js.typeOf “method” is magical when its argument is a member of a global scope object.

Scala.js emits ECMAScript 2015 code by default

Now that ES 2015 has been supported by major JS engines for a while, it was time to emit ES 2015 code by default. The ES 2015 output has several advantages over the older ES 5.1 strict mode output:

  • Throwables, by virtue of properly extending JavaScript’s Error, have an [[ErrorData]] internal slot, and therefore receive proper debugging info in JS engines, allowing better display of stack traces and error messages in interactive debuggers.
  • Static fields and methods in JS classes are properly inherited. See #2771.
  • The generated code is shorter.

To revert to emitting ES 5.1 strict mode code, use the following sbt setting:

scalaJSLinkerConfig in ThisBuild ~= { _.withESFeatures(_.withUseECMAScript2015(false)) }

With the default module kind NoModule, top-level exports are exported as top-level vars

In Scala.js 0.6.x, top-level exports such as

@JSExportTopLevel("Foo")
object Bar

were exported to JavaScript by being assigned to properties of the global object, for example as if by:

window.Foo = <the object Bar>; // or global.Foo, etc.

In Scala.js 1.x, they are exported as top-level JavaScript vars instead, as if by:

var Foo = <the object Bar>;

This ensures that code generated by Scala.js is completely generic with respect to which variables hold the global object, and hence works in any compliant JavaScript environment.

However, it also means that the generated .js file must be interpreted as a proper script for the variables to be visible by other scripts. This may have compatibility consequences.

js.UndefOr[A] is now an alias for A | Unit

Instead of defining js.UndefOr[+A] as its own type, it is now a simple type alias for A | Unit:

type UndefOr[+A] = A | Unit

The Option-like API is of course preserved.

We do not expect this to cause any significant issue, but it may impact type inference in subtle ways that can cause compile errors for previously valid code. You may have to adjust some uses of js.UndefOr due to these changes.

x eq y now more closely matches the JVM behavior

In Scala.js 0.6.x, x eq y always corresponds to JavaScript’s x === y. This is almost always correct, but causes issues when comparing +0.0 with -0.0 or NaN with itself.

In Scala.js 1.x, x eq y has been adapted to more closely match the JVM behavior, resulting in more portable code. It is equivalent to the old behavior except in the following cases:

  • +0.0 eq -0.0 is now false
  • NaN eq NaN is now true

The new behavior corresponds to JavaScript’s Object.is(x, y) function.

testHtml replaces both testHtmlFastOpt and testHtmlFullOpt

The separation of testHtmlFastOpt and testHtmlFullOpt, which were independent of the value of scalaJSStage, caused significant unfixable issues in 0.6.x. In Scala.js 1.x, both are replaced by a single task, testHtml. It is equivalent to the old testHtmlFastOpt if the value of scalaJSStage is FastOptStage (the default), and to testHtmlFullOpt if it is FullOptStage. This makes it more consistent with other tasks such as run and test.

Java system properties are not loaded from __ScalaJSEnv nor sbt’s javaOptions anymore

In 0.6.x, the sbt plugin automatically extracted -D options in javaOptions, and transferred them to the Java system properties API inside Scala.js, using a generated .js file filling in the magic variable __ScalaJSEnv. Scala.js 1.x does not support __ScalaJSEnv anymore, therefore -D options in javaOptions are now ignored.

Use your own mechanism to transfer data from the build to your Scala.js code, for example source code generation.

A unique, simple sbt setting to control what JS files are run or tested: jsEnvInput

By default, only the .js file generated by fastOptJS or fullOptJS is given to the selected jsEnv to be run or tested. In 0.6.x, there were several non-obvious task keys to modify this behavior: resolvedJSDependencies, loadedJSEnv and related. Moreover, the JSEnvs decided on their own, based on unclear heuristics, whether to treat the files as modules or not, and as what kind of module.

Scala.js 1.x consolidates all of that into one simple task key jsEnvInput of type Seq[org.scalajs.jsenv.Input], where an Input is an ADT with the following possible alternatives:

  • Input.Script(script): a JavaScript file to load as a script
  • Input.ESModule(module): a JavaScript file to load as an ECMAScript module (some JS envs may require a specific extension such as .mjs for this to work, due to limitations of the underlying engines)
  • Input.CommonJSModule(module): a JavaScript file to load as a CommonJS module

Not all JSEnvs support all kinds of Inputs.

The default value of jsEnvInput is a single Input whose type is derived from the ModuleKind, and which contains the output of fastOptJS or fullOptJS.

scalajs-javalib-ex was removed

The artifact scalajs-javalib-ex is removed in 1.x. It only contained a partial implementation of java.util.ZipInputStream. If you were using it, we recommend that you integrate a copy of its source code from Scala.js 0.6.x into your project.

js.use(x).as[T] was removed

The use cases for js.use(x).as[T] have been dramatically reduced by non-native JS classes (previously known as Scala.js-defined JS classes). This API seems virtually unused on the visible Web. Moreover, it was the only macro in the Scala.js standard library.

We have therefore removed it from the standard library, and it is not provided anymore. On demand, we can republish it as a separate library, if you need it.

The Tools API has been split into 3 artifacts and its packages reorganized

This only concerns consumers of the Tools API, i.e., tools that build on top of the Scala.js linker, such as ScalaFiddle. In Scala.js 0.6.x, all the tools were in one artifact scalajs-tools. This artifact has been split in two in Scala.js 1.x:

  • scalajs-logging: tiny logging API
  • scalajs-linker-interface: the extra stable interface for the linker API
  • scalajs-linker: the implementation of the linker API

The scalajs-linker artifact can be loaded via reflection, while developing against the stable APIs in scalajs-linker-interface.

In addition, the packages have been reorganized as follows:

  • org.scalajs.core.ir -> org.scalajs.ir
  • org.scalajs.core.tools.io -> gone (replaced by standard java.nio.file.Path-based APIs, and some abstractions in org.scalajs.linker)
  • org.scalajs.core.tools.logging -> org.scalajs.logging
  • org.scalajs.core.tools.linker -> org.scalajs.linker and org.scalajs.linker.interface

Additionally, the linker API has been refactored to be fully asynchronous in nature.

sbt 0.13.x is not supported anymore

You will not be able to use Scala.js 1.0.0 with sbt 0.13.x. You will have to upgrade to sbt 1.2.1 or later. As of this writing, the latest version is 1.3.8.

Scala 2.10.x, 2.11.{0-11} and 2.12.0 are not supported anymore

The title says it all: you cannot use Scala.js anymore with any of:

scalaVersion := "2.10.x" // for any x
scalaVersion := "2.11.x" // for 0 <= x <= 11
scalaVersion := "2.12.0"
  • Scala 2.10.x is not supported at all,
  • Only 2.11.12 is still supported in the 2.11.x series, and
  • 2.12.1 and following are supported in the 2.12.x series, but not 2.12.0.

Enhancements

There are very few enhancements in Scala.js 1.0.0. Scala.js 1.0.0 is focused on simplifying Scala.js, not on adding new features. Nevertheless, here are a few enhancements.

Scala.js can access require and other magical “global” variables of special JS environments

The changes from global object to global scope mean that magical “global” variables provided by some JavaScript environments, such as require in Node.js, are now visible to Scala.js. For example, it is possible to dynamically call require as follows in Scala.js 1.x:

val pathToSomeAsset = "assets/logo.png"
val someAsset = js.Dynamic.global.require(pathToSomeAsset)

We still recommend to use @JSImport and CommonJSModule for statically known imports.

Declaring inner classes in native JS classes

Some JavaScript APIs define classes inside objects, as in the following example:

class OuterClass {
  constructor(x) {
    this.InnerClass = class InnerClass {
      someMethod() {
        return x;
      }
    }
  }
}

allowing use sites to instantiate them as

const outerObject = new OuterClass(42);
const innerObject = new outerObject.InnerClass();
console.log(innerObject.someMethod()); // prints 42

In Scala.js 0.6.x, it is very awkward to define a facade type for OuterClass, as illustrated in issue #2398. Scala.js 1.x now allows to declare them very easily as inner JS classes:

@js.native
@JSGlobal
class OuterClass(x: Int) extends js.Object {
  @js.native
  class InnerClass extends js.Object {
    def someMethod(): Int = js.native
  }
}

which in turns allows for the following call site:

val outerObject = new OuterClass(42)
val innerObject = new outerObject.InnerClass()
console.log(innerObject.someMethod()) // prints 42

Nested non-native JS classes expose sane constructors to JavaScript

It is now possible to declare non-native JS classes inside outer classes or inside defs, and use their js.constructorOf in a meaningful way. For example, one can define a method that creates a new JavaScript class every time it is invoked:

def makeGreeter(greetingFormat: String): js.Dynamic = {
  class Greeter extends js.Object {
    def greet(name: String): String =
      println(greetingFormat.format(name))
  }
  js.constructorOf[Greeter]
}

Assuming there is some native JavaScript function like

function greetPeople(greeterClass) {
  const greeter = new greeterClass();
  greeter.greet("Jane");
  greeter.greet("John");
}

one could call it from Scala.js as:

val englishGreeterClass = makeGreeter("Hello, %s!")
greetPeople(englishGreeterClass)
val frenchGreeterClass = makeGreeter("Bonjour, %s!")
greetPeople(frenchGreeterClass)
val japaneseGreeterClass = makeGreeter("%sさん、こんにちは。")
greetPeople(japaneseGreeterClass)

resulting in the following output:

Hello, Jane!
Hello, John!
Bonjour, Jane!
Bonjour, John!
Janeさん、こんにちは。
Johnさん、こんにちは。

In Scala.js 0.6.x, the above code would compile but produce incoherent results at run-time, because js.constructorOf was meaningless for nested classes.

Bugfixes

Amongst others, the following bugs have been fixed since 0.6.32:

  • #2800 Global lets, consts and classes cannot be accessed by Scala.js
  • #2382 Name clash for $outer pointers of two different nesting levels (fixed for Scala 2.10 and 2.11; 2.12 did not suffer from the bug in 0.6.x)
  • #3085 Linking error after the optimizer for someInt.toDouble.compareTo(double)

See the full list of issues fixed in each of the milestones and release candidates of 1.0.0 on GitHub:

Cross-building for Scala.js 0.6.x and 1.x

If you want to cross-compile your libraries for Scala.js 0.6.x and 1.x, here are a couple tips.

Dynamically load a custom version of Scala.js

Since the version of Scala.js is not decided by an sbt setting in build.sbt, but by the version of the sbt plugin in project/plugins.sbt, standard cross-building setups based on ++ cannot be applied. We recommend that you load the version of Scala.js from an environment variable. For example, you can do this in your project/plugins.sbt file:

val scalaJSVersion =
  Option(System.getenv("SCALAJS_VERSION")).getOrElse("1.0.0")

addSbtPlugin("org.scala-js" % "sbt-scalajs" % scalaJSVersion)

You can then launch

$ sbt

from your command line to start up your build with Scala.js 1.0.0 by default, or

$ SCALAJS_VERSION=0.6.32 sbt

to use an older version of Scala.js.

Extra dependencies for JS environments

You can further build on the above val scalaJSVersion to dynamically add dependencies on scalajs-env-phantomjs and/or scalajs-env-jsdom-nodejs if you use them:

// For Node.js with jsdom
libraryDependencies ++= {
  if (scalaJSVersion.startsWith("0.6.")) Nil
  else Seq("org.scala-js" %% "scalajs-env-jsdom-nodejs" % "1.0.0")
}

// For PhantomJS
{
  if (scalaJSVersion.startsWith("0.6.")) Nil
  else Seq(addSbtPlugin("org.scala-js" % "sbt-scalajs-env-phantomjs" % "1.0.0"))
}

// For Selenium
libraryDependencies += {
  val v = if (scalaJSVersion.startsWith("0.6.")) "0.3.0" else "1.0.0"
  libraryDependencies += "org.scala-js" %% "scalajs-env-selenium" % v
}

In all cases, you can then use the source-compatible API in build.sbt to select your JS environment of choice.

Extra dependencies for jsDependencies

Similarly, you can conditionally depend on jsDependencies as follows:

// For jsDependencies
{
  if (scalaJSVersion.startsWith("0.6.")) Nil
  else Seq(addSbtPlugin("org.scala-js" % "sbt-jsdependencies" % "1.0.0"))
}

In that case, you should unconditionally keep the

enablePlugins(JSDependenciesPlugin)

on the relevant projects. Scala.js 0.6.20 and later define a no-op JSDependenciesPlugin to allow for this scenario.

Conditional application of -P:scalajs:sjsDefinedByDefault

In Scala.js 1.x, the flag -P:scalajs:sjsDefinedByDefault has been removed. However, if you have non-native JS types in your codebase, you need this flag in Scala.js 0.6.x.

Add the following setting to your build.sbt to conditionally enable that flag:

scalacOptions ++= {
  if (scalaJSVersion.startsWith("0.6.")) Seq("-P:scalajs:sjsDefinedByDefault")
  else Nil
}