summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLi Haoyi <haoyi@dropbox.com>2014-11-09 10:08:47 -0800
committerLi Haoyi <haoyi@dropbox.com>2014-11-09 10:08:47 -0800
commit978a138c02c07822ef71f31f71e552a9659a0a53 (patch)
tree9771e4d4620af7e6f5ff54cb4c711e04cffb4e30
parent795c0eb5de003b22c3874762557ae2b34ae64de0 (diff)
downloadhands-on-scala-js-978a138c02c07822ef71f31f71e552a9659a0a53.tar.gz
hands-on-scala-js-978a138c02c07822ef71f31f71e552a9659a0a53.tar.bz2
hands-on-scala-js-978a138c02c07822ef71f31f71e552a9659a0a53.zip
wip
-rwxr-xr-xbook/src/main/resources/css/layouts/side-menu.css4
-rw-r--r--book/src/main/scala/book/Book.scala24
-rw-r--r--book/src/main/scala/book/Utils.scala3
-rw-r--r--book/src/main/scalatex/book/Index.scalatex14
-rw-r--r--book/src/main/scalatex/book/handson/GettingStarted.scalatex18
-rw-r--r--book/src/main/scalatex/book/handson/PublishingModules.scalatex204
-rw-r--r--book/src/main/scalatex/book/indepth/DesignSpace.scalatex68
-rw-r--r--book/src/main/scalatex/book/indepth/JavaAPIs.scalatex38
-rw-r--r--examples/crossBuilds/simple/build.sbt13
-rw-r--r--examples/crossBuilds/simple2/build.sbt13
-rw-r--r--examples/crossBuilds/simple2/js/shared/main/scala/simple/Simple.scala7
-rw-r--r--examples/crossBuilds/simple2/js/shared/test/scala/simple/SimpleTest.scala13
-rw-r--r--examples/crossBuilds/simple2/js/src/main/scala/simple/Platform.scala9
l---------examples/crossBuilds/simple2/jvm/shared1
-rw-r--r--examples/crossBuilds/simple2/jvm/src/main/scala/simple/Platform.scala13
-rw-r--r--examples/crossBuilds/simple2/project/build.sbt4
-rw-r--r--project/build.sbt4
17 files changed, 373 insertions, 77 deletions
diff --git a/book/src/main/resources/css/layouts/side-menu.css b/book/src/main/resources/css/layouts/side-menu.css
index d6e4ff1..4fcc658 100755
--- a/book/src/main/resources/css/layouts/side-menu.css
+++ b/book/src/main/resources/css/layouts/side-menu.css
@@ -250,6 +250,6 @@ Hides the menu at `48em`, but modify this based on your app's needs.
}
/*Workaround for bug in highlight.js IDEA theme*/
-.hljs-tag{
+.hljs-tag, .hljs-symbol{
background: none;
-} \ No newline at end of file
+}
diff --git a/book/src/main/scala/book/Book.scala b/book/src/main/scala/book/Book.scala
index ad52177..785e6ab 100644
--- a/book/src/main/scala/book/Book.scala
+++ b/book/src/main/scala/book/Book.scala
@@ -135,4 +135,28 @@ object Book {
pre(code(cls:=lang + " highlight-me", blob))
}
}
+ import java.io.File
+ def recursiveListFiles(f: File): Array[File] = {
+ val these = f.listFiles
+ these ++ these.filter(_.isDirectory).flatMap(recursiveListFiles)
+ }
+ lazy val javaAPIs = {
+ val roots = Seq(
+ "output/scala-js/javalanglib/src/main/scala",
+ "output/scala-js/javalib/src/main/scala"
+ )
+ for{
+ root <- roots
+ file <- recursiveListFiles(new File(root))
+ if file != null
+ if file.isFile
+ } yield{
+ val path = file.getPath
+ .drop(root.length + 1)
+ .dropRight(".scala".length)
+ val filename = path.replace('/', '.')
+ val docpath = s"https://docs.oracle.com/javase/7/docs/api/$path.html"
+ filename -> docpath
+ }
+ }
}
diff --git a/book/src/main/scala/book/Utils.scala b/book/src/main/scala/book/Utils.scala
index d5272a6..4738324 100644
--- a/book/src/main/scala/book/Utils.scala
+++ b/book/src/main/scala/book/Utils.scala
@@ -72,7 +72,8 @@ object Utils{
def apply(args: Frag*) = {
val wrappedContents = contentWrap.getOrElse((x: Frag) => x)(args)
val res = Seq[Frag](
- headerWrap(name, subname)(cls:="content-subhead", id:=munge(name)),
+ if (name == "") ""
+ else headerWrap(name, subname)(cls:="content-subhead", id:=munge(name)),
wrappedContents
)
indent -= 1
diff --git a/book/src/main/scalatex/book/Index.scalatex b/book/src/main/scalatex/book/Index.scalatex
index fba24f7..3cb8c2a 100644
--- a/book/src/main/scalatex/book/Index.scalatex
+++ b/book/src/main/scalatex/book/Index.scalatex
@@ -14,9 +14,16 @@
@p
This book contains something for all levels of experience with Scala.js: absolute beginners can get started with the Introduction and Hands-on tutorial, people who have used it before can skip ahead to the later parts of the tutorial, building a canvas app or dynamic HTML page. Intermediate users will find the chapters on cross-publishing a Scala.js library interesting, and even experienced users will find the In-depth Documention useful.
+ @p
+ This book does not spend time on pontifying a philosophy or ideology behind Scala.js or Scala. It instead spends its words on hands-on tutorials and in-depth dives into parts of the Scala.js platform, to try and get you acquainted with Scala.js as soon as possible, so you can make your own decisions about its merits or qualities.
+
@sect("Intro to Scala.js")
@Intro.template
+@sect("Hands On", "Writing your first Scala.js programs")
+ @p
+ This half of the book walks you through various facets of the Scala.js development experience. From making your first app, to testing and publishing modules, to writing an integrated client-server application.
+
@sect("Getting Started")
@handson.GettingStarted.template
@@ -37,7 +44,7 @@
@sect("Scala.js in Depth", "Exploring Scala.js")
@p
- This half of the book dives into a few aspects of Scala.js much more deeply that the hands-on introduction does. It's aimed at someone who has already used Scala.js, and wants to explore the edge-cases or how things work under-the-cover. It's not a formal specification; rather, it's aim is to be a useful reference to read instead of (or in preparation for) digging into the implementation code.
+ This half of the book dives into a few aspects of Scala.js much more deeply that the hands-on introduction does. It's aimed at someone who has already used Scala.js, and wants to explore the edge-cases, how things work under-the-cover, or why it has been designed in such a way. It's not a formal specification; rather, it's aim is to be a useful reference to read instead of (or in preparation for) digging into the implementation code.
@sect("Javascript Interoperability")
@indepth.JavascriptInterop.template
@@ -49,4 +56,7 @@
@indepth.CompilationPipeline.template
@sect("Scala.js' Design Space")
- @indepth.DesignSpace.template \ No newline at end of file
+ @indepth.DesignSpace.template
+
+ @sect("Java APIs")
+ @indepth.JavaAPIs.template \ No newline at end of file
diff --git a/book/src/main/scalatex/book/handson/GettingStarted.scalatex b/book/src/main/scalatex/book/handson/GettingStarted.scalatex
index 12f0f28..ae97eb2 100644
--- a/book/src/main/scalatex/book/handson/GettingStarted.scalatex
+++ b/book/src/main/scalatex/book/handson/GettingStarted.scalatex
@@ -98,17 +98,17 @@
@p
We've downloaded, compiled, ran, and made changes to our first Scala.js application. Let's now take a closer look at the code that we just ran:
- @hl.ref("output/temp/src/main/scala/example/ScalaJSExample.scala")
+ @hl.ref("output/workbench-example-app/src/main/scala/example/ScalaJSExample.scala")
@p
It's a good chunk of code, though not a huge amount. To someone who didn't know about Scala.js, they would just think it's normal Scala, albeit with this unusual @hl.scala{dom} library and a few weird annotations. Let's pick it apart starting from the top:
- @hl.ref("output/temp/src/main/scala/example/ScalaJSExample.scala", "case class Point", "@JSExport")
+ @hl.ref("output/workbench-example-app/src/main/scala/example/ScalaJSExample.scala", "case class Point", "@JSExport")
@p
Here we are defining a @hl.scala{Point} case class which represents a X/Y position, with some basic operators defined on it. This is done mostly for convenience later on, when we want to manipulate these two-dimensional points. Scala.js is Scala, and supports the entirety of the Scala language. @hl.scala{Point} here behaves identically as it would if you had run Scala on the JVM.
- @hl.ref("output/temp/src/main/scala/example/ScalaJSExample.scala", "@JSExport", "val ctx")
+ @hl.ref("output/workbench-example-app/src/main/scala/example/ScalaJSExample.scala", "@JSExport", "val ctx")
@p
This @hl.scala("@JSExport") annotation is used to tell Scala.js that you want this method to be visible and callable from Javascript. By default, Scala.js does dead code elimination and removes any methods or classes which are not used. This is done to keep the compiled executables a reasonable size, since most projects use only a small fraction of e.g. the standard library. @hl.scala("@JSExport") is used to tell Scala.js that the @hl.scala{ScalaJSExample} object and its @hl.scala{def main} method are entry points to the program. Even if they aren't called anywhere internally, they are called externally by Javascript that the Scala.js compiler is not aware of, and should not be removed.
@@ -116,7 +116,7 @@
@p
Apart from this annotation, @hl.scala{ScalaJSExample} is just a normal Scala @hl.scala{object}, and behaves like one in every way. Note that the main-method in this case takes a @hl.scala{dom.HTMLCanvasElement}: your exported methods can have any signature, with arbitrary arity or types for parameters or the return value. This is in contrast to the main method on the JVM which always takes an @hl.scala{Array[String]} and returns @hl.scala{Unit}. In fact, there's nothing special about this method at all! It's like any other exported method, we just happen to attribute it the "main" entry point.
- @hl.ref("output/temp/src/main/scala/example/ScalaJSExample.scala", "val ctx", "var count")
+ @hl.ref("output/workbench-example-app/src/main/scala/example/ScalaJSExample.scala", "val ctx", "var count")
@p
Here we are retrieving a handle to the canvas we will draw on using @hl.scala{document.getElementById}, and from it we can get a @hl.scala{dom.CanvasRenderingContext2D} which we actually use to draw on it.
@@ -127,7 +127,7 @@
@p
We need to perform the @hl.scala{asInstanceOf} call because depending on what you pass to @hl.scala{getElementById} and @hl.scala{getContext}, you could be returned elements and contexts of different types. Hence we need to tell the compiler explicitly that we're expecting a @hl.scala{dom.HTMLCanvasElement} and @hl.scala{dom.CanvasRenderingContext2D} back from these methods for the strings we passed in.
- @hl.ref("output/temp/src/main/scala/example/ScalaJSExample.scala", "def run", "dom.setInterval")
+ @hl.ref("output/workbench-example-app/src/main/scala/example/ScalaJSExample.scala", "def run", "dom.setInterval")
@p
This is the part of the Scala.js program which does the real work. It runs 10 iterations of a @a("small algorithm", href:="http://en.wikipedia.org/wiki/Sierpinski_triangle#Chaos_game") that generates a Sierpinski Triangle point-by-point. The steps, as described by the linked article, are roughly:
@@ -145,7 +145,7 @@
@p
In this example, the triangle is hard-coded to be 255 pixels high by 255 pixels wide, and some math is done to pick a color for each dot which will give the triangle a pretty gradient.
- @hl.ref("output/temp/src/main/scala/example/ScalaJSExample.scala", "dom.setInterval")
+ @hl.ref("output/workbench-example-app/src/main/scala/example/ScalaJSExample.scala", "dom.setInterval")
@p
Now this is the call that actually does the useful work. All this method does is call @hl.scala{dom.setInterval}, which tells the browser to run the @hl.scala{run} method every 50 milliseconds. As mentioned earlier, the @hl.scala{dom.*} methods are simply facades to their native Javascript equivalents, and @hl.scala{dom.setInterval} is @a("no different", href:="https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers.setInterval"). Note how you can pass a Scala lambda to @hl.scala{setInterval} to have it called by the browser, where in Javascript you'd need to pass a Javascript @hl.javascript{function(){...}}
@@ -156,7 +156,7 @@
We've already taken a look at the application code for a simple, self-contained Scala.js application, but this application is not @i{entirely} self contained. It's wrapped in a small SBT project that sets up the necessary dependencies and infrastructure for this application to work.
@sect{project/build.sbt}
- @hl.ref("output/temp/project/build.sbt")
+ @hl.ref("output/workbench-example-app/project/build.sbt")
@p
This is the list of SBT plugins used by this small example application. There are two of them: the Scala.js plugin (which contains the Scala.js compiler and other things, e.g. tasks such as @code{fastOptJS}) and the @a("Workbench", href:="https://github.com/lihaoyi/workbench") plugin, which is used to provide the auto-reload-on-change behavior and the forwarding of SBT logspam to the browser console.
@@ -166,7 +166,7 @@
@sect{build.sbt}
- @hl.ref("output/temp/build.sbt")
+ @hl.ref("output/workbench-example-app/build.sbt")
@p
The @code{build.sbt} project file for this application is similarly unremarkable: It includes the settings for the two SBT plugins we saw earlier, as well as boilerplate @hl.scala{name}/@hl.scala{version}/@hl.scala{scalaVersion} values common to all projects.
@@ -178,7 +178,7 @@
Lastly, we have two Workbench related settings: @hl.scala{bootSnippet} basically tells Workbench how to restart your application when a new compilation run finishes, and @hl.scala{updateBrowsers} actually tells it to perform this application-restarting.
@sect{src/main/resources/index-dev.html}
- @hl.ref("output/temp/src/main/resources/index-dev.html")
+ @hl.ref("output/workbench-example-app/src/main/resources/index-dev.html")
@p
This is the HTML page which our toy app lives in, and the same page that we have so far been using to view the app in the browser. To anyone who has used HTML, most of it is probably familiar. Things of note are the Script tags: @hl.scala{"../example-fastopt.js"} Is the executable blob spat out by the compiler, which we need to include in the HTML page for anything to happen. This is where the results of your compiled Scala code appear. @hl.scala{"workbench.js"} is the client for the Workbench plugin that connects to SBT, reloads the browser and forwards logspam to the browser console.
diff --git a/book/src/main/scalatex/book/handson/PublishingModules.scalatex b/book/src/main/scalatex/book/handson/PublishingModules.scalatex
index 3e009d3..387d120 100644
--- a/book/src/main/scalatex/book/handson/PublishingModules.scalatex
+++ b/book/src/main/scalatex/book/handson/PublishingModules.scalatex
@@ -4,10 +4,8 @@
@p
Not all code is developed in the browser. Maybe you want to run simple snippets of Scala.js which don't interact with the browser at all, and having to keep a browser open is an overkill. Maybe you want to write unit tests for your browser-destined code, so you can verify that it works without firing up Chrome. Maybe it's not a simple script but a re-distributable library, and you want to run the same command-line unit tests on both Scala.js and Scala-JVM to verify that the behavior is identical. This chapter will go through all these cases.
-@sect{A Scala.js Module}
- TODO
-@sect{A Cross-Built Module}
+@sect{A Simple Cross-Built Module}
@p
As always, we will start with an example: in this case a toy library whose sole purpose in life is to take a series of timestamps (milliseconds UTC) and format them into a single, newline-delimited string. This is what the project layout looks like:
@@ -42,68 +40,170 @@
@hl.bash
$ ln -s ../js/shared jvm/shared
- @p
- From the bash shell in the project root. Let's take a look at the various files that make up this project. First, the @code{build.sbt} files:
-
- @hl.ref("examples/crossBuilds/simple/project/build.sbt")
-
- @p
- The @code{project/build.sbt} file is uneventful: it simply includes the Scala.js SBT plugin. However, the @code{build.sbt} file is a bit more interesting:
-
- @hl.ref("examples/crossBuilds/simple/build.sbt")
+ @sect{Build Configuration}
+ @p
+ From the bash shell in the project root. Let's take a look at the various files that make up this project. First, the @code{build.sbt} files:
+
+ @hl.ref("examples/crossBuilds/simple/project/build.sbt")
+
+ @p
+ The @code{project/build.sbt} file is uneventful: it simply includes the Scala.js SBT plugin. However, the @code{build.sbt} file is a bit more interesting:
+
+ @hl.ref("examples/crossBuilds/simple/build.sbt")
+
+ @p
+ Unlike the equivalent @code{build.sbt} files you saw in earlier chapters, this does not simply add @hl.scala{scalaJSSettings} to the root project. Rather, it sets up two projects: one in the @code{js/} folder and one in the @code{jvm/} folder, with the @code{js/} version getting the settings from the Scala.js plugin. To both of these, we add @code{shared/main/scala} to the list of source directories. This means that both projects will pick up the sources we symlinked between @code{js/shared/} and @code{jvm/shared/}.
+
+ @sect{Source Files}
+
+ @p
+ Now, let's look at the contents of the @code{.scala} files that make up the meat of this project:
+
+ @hl.ref("examples/crossBuilds/simple/js/shared/main/scala/simple/Simple.scala")
+
+ @p
+ In @code{Simple.scala} we have the shared, cross-platform API of our library: a single @hl.scala{object} with a single method @hl.scala{def} which does what we want, which can then be used in either Scala.js or Scala-JVM. In general, you can put as much shared logic here as you want: classes, objects, methods, anything that can run on both Javascript and on the JVM.
+
+ @p
+ However, when it comes to actually formatting the date, we have a problem: Javascript and Java provide different utilities for formatting dates! They both let you format them, but they provide different APIs. Thus, to do the formatting of each individual date, we call out to the @hl.scala{Platform.format} function, which is implemented twice: once in @code{js/} and once in @code{jvm/}:
+
+ @div(cls:="pure-g")
+ @div(cls:="pure-u-1 pure-u-md-1-2")
+ @hl.ref("examples/crossBuilds/simple/js/src/main/scala/simple/Platform.scala")
+
+ @div(cls:="pure-u-1 pure-u-md-1-2")
+ @hl.ref("examples/crossBuilds/simple/jvm/src/main/scala/simple/Platform.scala")
+
+ @p
+ In the @code{js/} version, we are using the Javascript @hl.javascript{Date} object to take the millis and do what we want. In the @code{jvm/} version, we instead use @hl.scala{java.text.SimpleDateFormat} with a custom formatter (The syntax is defined @a("here", href:="http://docs.oracle.com/javase/7/docs/api/java/text/SimpleDateFormat.html")).
+
+ @p
+ Again, you can put as much platform-specific logic in these files as you want, to account for differences in the available APIs. Maybe you want to use @hl.scala{js.JSON.parse} for parsing JSON blobs in @code{js/}, but @hl.scala{Jackson} or @hl.scala{GSON} for parsing them in @code{jvm/}.
+ @p
+ Lastly, you'll also have noticed the two identical @hl.scala{main} methods in the platform-specific code. This is an implementation detail around the fact that Scala.js picks up the main method differently from Scala-JVM, using @hl.scala{js.JSApp} instead of looking for a @hl.scala{main(args: Array[String]): Unit} method. These two main methods allow us to test our implementations
+ @sect{Running the Module}
+ @hl.bash
+ > ; js/run; jvm/run
+ [info] Running simple.Platform
+ Running on JS! 1
+ November 2, 2014 2:58:48 PM PST
+ November 2, 2014 2:58:49 PM PST
+ [success] Total time: 4 s, completed Nov 2, 2014 2:58:48 PM
+ [info] Running simple.Platform
+ Running on JVM! 1.0
+ November 2, 2014 2:58:49 PM PST
+ November 2, 2014 2:58:50 PM PST
+ [success] Total time: 0 s, completed Nov 2, 2014 2:58:49 PM
+
+ @p
+ As you can see, both runs printed the same results, modulo three things:
+
+ @ul
+ @li
+ The "Running on XXX!" statement which shows us we're actually running on two platforms.
+ @li
+ The other hint is the time taken: the second run is instant while the first takes three seconds! This is because by default we run on @a("Rhino", href:="https://developer.mozilla.org/en-US/docs/Mozilla/Projects/Rhino"), which is a simple interpreter hundreds of times slower than running code natively on the JVM.
+ @li
+ In Scala-JVM the double 1.0 is printed as @code{1.0}, while in Scala.js it's printed as @code{1}. This is one of a small number of differences between Scala.js and Scala-JVM, and verifies that we are indeed running on both platforms!
+
+ @p
+ You've by this point set up a basic cross-building Scala.js/Scala-JVM project!
+
+ @hr
@p
- Unlike the equivalent @code{build.sbt} files you saw in earlier chapters, this does not simply add @hl.scala{scalaJSSettings} to the root project. Rather, it sets up two projects: one in the @code{js/} folder and one in the @code{jvm/} folder, with the @code{js/} version getting the settings from the Scala.js plugin. To both of these, we add @code{shared/main/scala} to the list of source directories. This means that both projects will pick up the sources we symlinked between @code{js/shared/} and @code{jvm/shared/}.
+ If you wish, you can do more things with this project you've set up:
- @p
- Now, let's look at the contents of the @code{.scala} files that make up the meat of this project:
-
- @hl.ref("examples/crossBuilds/simple/js/shared/main/scala/simple/Simple.scala")
-
- @p
- In @code{Simple.scala} we have the shared, cross-platform API of our library: a single @hl.scala{object} with a single method @hl.scala{def} which does what we want, which can then be used in either Scala.js or Scala-JVM. In general, you can put as much shared logic here as you want: classes, objects, methods, anything that can run on both Javascript and on the JVM.
+ @ul
+ @li
+ Flesh it out! Currently this module only does a single, trivial thing. If you've done any web development before, I'm sure you can find some code snippet, function or algorithm that you'd like to share between client and server. Try implementing it in the @code{shared/} folder to be usable in both Scala.js and Scala-JVM
+ @li
+ Publish it! Both @code{sbt publishLocal} and @code{sbt publishSigned} work on this module, for publishing either locally, Maven Central via Sonatype, or Bintray. Running the command bare should be sufficient to publish both the @code{js} or @code{jvm} projects, or you can also specify which one e.g. @code{jvm/publishLocal} to publish only one subproject.
@p
- However, when it comes to actually formatting the date, we have a problem: Javascript and Java provide different utilities for formatting dates! They both let you format them, but they provide different APIs. Thus, to do the formatting of each individual date, we call out to the @hl.scala{Platform.format} function, which is implemented twice: once in @code{js/} and once in @code{jvm/}:
-
- @div(cls:="pure-g")
- @div(cls:="pure-u-1 pure-u-md-1-2")
- @hl.ref("examples/crossBuilds/simple/js/src/main/scala/simple/Platform.scala")
+ This @code{jvm} project works identically to any other Scala-JVM project, and the @code{js} project works identically to the Command Line API described earlier. Thus you can do things like @code{fastOptStage::run} to run the code on Node.js, setting @hl.scala{requiresDOM := true}, run @code{fullOptStage::run} to run the code with full, aggressive optimizations. And of course, things that work in both Scala.js and Scala-JVM can be run on both, basic commands such as code{run} or @code{test}.
- @div(cls:="pure-u-1 pure-u-md-1-2")
- @hl.ref("examples/crossBuilds/simple/jvm/src/main/scala/simple/Platform.scala")
-
- @p
- In the @code{js/} version, we are using the Javascript @hl.javascript{Date} object to take the millis and do what we want. In the @code{jvm/} version, we instead use @hl.scala{java.text.SimpleDateFormat} with a custom formatter (The syntax is defined @a("here", href:="http://docs.oracle.com/javase/7/docs/api/java/text/SimpleDateFormat.html")).
-
@p
- Again, you can put as much platform-specific logic in these files as you want, to account for differences in the available APIs. Maybe you want to use @hl.scala{js.JSON.parse} for parsing JSON blobs in @code{js/}, but @hl.scala{Jackson} or @hl.scala{GSON} for parsing them in @code{jvm/}.
+ You can also run tests using this code, if you have a testing library set up. The next section will go into detail as to how to set that up.
+
+@sect{Cross-Testing with uTest}
@p
- Lastly, you'll also have noticed the two identical @hl.scala{main} methods in the platform-specific code. This is an implementation detail around the fact that Scala.js picks up the main method differently from Scala-JVM, using @hl.scala{js.JSApp} instead of looking for a @hl.scala{main(args: Array[String]): Unit} method. These two main methods allow us to test our implementations
-
- @hl.bash
- > ; js/run; jvm/run
- [info] Running simple.Platform
- Running on JS! 1
- November 2, 2014 2:58:48 PM PST
- November 2, 2014 2:58:49 PM PST
- [success] Total time: 4 s, completed Nov 2, 2014 2:58:48 PM
- [info] Running simple.Platform
- Running on JVM! 1.0
- November 2, 2014 2:58:49 PM PST
- November 2, 2014 2:58:50 PM PST
- [success] Total time: 0 s, completed Nov 2, 2014 2:58:49 PM
+ @a("uTest", href:="https://github.com/lihaoyi.utest") is a small unit-testing library for Scala programs, that works on both Scala-JVM and Scala.js. At the time it was written, it was the first one out there, though now there are others such as @a("little-spec", href:="https://github.com/eecolor/little-spec") or @a("otest", href:="https://github.com/cgta/otest"). Notably, Scala's traditional testing libraries such as @a("Scalatest", href:="http://www.scalatest.org/") or @a("Specs2", href:="http://etorreborre.github.io/specs2/") do not work with Scala.js, as they make use of Reflection or other things not supported on Scala.js
+
+ @sect{uTest Configuration}
+ @p
+ To make your code use uTest, there are a few changes you need to make. First, you need to add the uTest SBT plugin:
+
+ @hl.ref("examples/crossBuilds/simple2/project/build.sbt")
+
+ @p
+ Here, in @code{project/build.sbt}, we see it used next to the Scala.js SBT plugin. Next, we need to modify our @code{build.sbt} file
+
+ @hl.ref("examples/crossBuilds/simple2/build.sbt")
+
+ @p
+ The main thing we've done is make use of uTest's @hl.scala{JsCrossBuild}: this does the work we've previously done to setup the shared-source-directory in SBT, as well as doing the neccessary configuration for uTest itself, providing you with ready-made @hl.scala{js} and @hl.scala{jvm} projects you can work with.
+
+ @sect{Your First Tests!}
+ @p
+ Lastly, we need to start writing tests! @a("uTest", href:="https://github.com/lihaoyi.utest") is well documented, but to get started here's a simple test suite for our @hl.scala{formatDates} function:
+
+ @hl.ref("examples/crossBuilds/simple2/js/shared/test/scala/simple/SimpleTest.scala")
+
+ @p
+ Since this is in @code{shared/}, it is automatically symlinked and is picked up by both @code{js} and @code{jvm} subprojects. With that done, you just need to run the @code{test} commands:
+
+ @hl.bash
+ > ; js/test; jvm/test
+ [info] 1/4 simple.SimpleTest.format.nil Success
+ [info] 2/4 simple.SimpleTest.format.timeZero Success
+ [info] 3/4 simple.SimpleTest.format Success
+ [info] 4/4 simple.SimpleTest Success
+ [info] -----------------------------------Results-----------------------------------
+ [info] simple.SimpleTest Success
+ [info] format Success
+ [info] nil Success
+ [info] timeZero Success
+ [info] Failures:
+ [info]
+ [info] Tests: 4
+ [info] Passed: 4
+ [info] Failed: 0
+ [success] Total time: 4 s, completed Nov 8, 2014 7:42:39 PM
+ [info] 1/4 simple.SimpleTest.format.nil Success
+ [info] 2/4 simple.SimpleTest.format.timeZero Success
+ [info] 3/4 simple.SimpleTest.format Success
+ [info] 4/4 simple.SimpleTest Success
+ [info] -----------------------------------Results-----------------------------------
+ [info] simple.SimpleTest Success
+ [info] format Success
+ [info] nil Success
+ [info] timeZero Success
+ [info] Failures:
+ [info]
+ [info] Tests: 4
+ [info] Passed: 4
+ [info] Failed: 0
+ [success] Total time: 0 s, completed Nov 8, 2014 7:42:39 PM
+
+ @p
+ And you'll see that we've run our unit tests twice: once on Scala.js in Rhino, and once on Scala-JVM! As expected, the first run in Rhino took much longer (4 seconds!) than the second, as Rhino is much slower than running code directly on the JVM. You can configure the Scala.js run to run in Node.js or PhantomJS, as well as running under different optimization levels.
+
+ @hr
@p
- As you can see, both runs printed the same results, modulo three things:
+ Now that you've got a basic cross-platform Scala module building and testing, what next? One thing you may want to do is add things to the project. Depending on where you want your code to run, there's a place for everythign:
@ul
- @li
- The "Running on XXX!" statement which shows us we're actually running on two platforms.
@li
- The other hint is the time taken: the second run is instant while the first takes three seconds! This is because by default we run on @a("Rhino", href:="https://developer.mozilla.org/en-US/docs/Mozilla/Projects/Rhino"), which is a simple interpreter hundreds of times slower than running code natively on the JVM.
+ @code{js/shared/main/scala}/@code{jvm/shared/main/scala} is where your shared library code goes. This code will be run on both Scala.js and Scala-JVM
+ @li
+ @code{jvm/src/main/scala} Scala-JVM only code
@li
- In Scala-JVM the double 1.0 is printed as @code{1.0}, while in Scala.js it's printed as @code{1}. This is one of a small number of differences between Scala.js and Scala-JVM, and verifies that we are indeed running on both platforms!
+ @code{js/src/main/scala} Scala.js only code
+
+ @p
+ It is entirely possible for your modules to have slightly different implementations and APIs on Scala.js and Scala-JVM. @a("Scalatags", href:="https://github.com/lihaoyi/scalatags") exposes additional DOM-related functionality only for it's Scala.js version, while @a("uPickle", href:="https://github.com/lihaoyi/upickle") uses different JSON libraries (@a("Jawn", href:="https://github.com/non/jawn") v.s. @a("DOM", href:="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse")) on the backend while the exposed interface remains the same. You have the flexibility to pick and choose which bits of your library you wish to share and which bits will be different.
@p
- You've by this point set up a basic cross-building Scala.js/Scala-JVM project!
+ Everything above also applies to your unit tests, which fall in @code{test/} folders mirroring the @code{main/} folders listed above. You can also choose to share or not-share your unit test code as you see fit. \ No newline at end of file
diff --git a/book/src/main/scalatex/book/indepth/DesignSpace.scalatex b/book/src/main/scalatex/book/indepth/DesignSpace.scalatex
index 2a41c4c..abdd655 100644
--- a/book/src/main/scalatex/book/indepth/DesignSpace.scalatex
+++ b/book/src/main/scalatex/book/indepth/DesignSpace.scalatex
@@ -122,12 +122,72 @@
@p
Using macros here also plays well with the Scala.js optimizer: the macros are fully expanded before the optimizer is run, so by the time the optimizer sees the code, there is no more magic left: it is then free to do dead-code-elimination/inlining/other-optimizations without worrying about reflection causing the code to do weird things at runtime. Thus, we've managed to substitute most of the main use-cases of reflection, and so can do without it.
-@sect("Why No inline-Javascript?")
- TODO
+@sect{Why does error behavior differ?}
+ @p
+ Scala.js deviates from the semantics of Scala-JVM in several ways. Many of these ways revolve around the edge-conditions of a program: what happens when something goes wrong? An array index is out of bounds? An integer is divided-by-zero? These differences cause some amount of annoyance when debugging, since when you mess up an array index, you expect an exception, not silently-invalid-data!
+
+ @p
+ In most of these cases, it was a trade-off between performance and correctness. These are situations where the default semantics of Scala deviate from that of Javascript, and Scala.js would have to perform extra work to emulate the desired behavior. For example, compare the division behavior of the JVM and Javascript.
+ @sect{Divide-by-zero: a case study}
+ @hl.scala
+ /*JVM*/
+ 15 / 4 // 3
+ @hl.javascript
+ /*JS*/
+ 15 / 4 // 3.25
+ @p
+ On the JVM, integer division is a primitive, and dividing @hl.scala{15 / 4} gives @hl.scala{3}. However, in Javascript, it gives @hl.javascript{3.25}, since all numbers of double-precision floating points.
+
+ @p
+ Scala.js works around this in the general case by adding a @hl.javascript{| 0} to the translation, e.g.
+
+ @hl.scala
+ /*JVM*/
+ 15 / 4 // 3
+ @hl.javascript
+ /*JS*/
+ (15 / 4) | 0 // 3
-@sect("Why does error behavior differ?")
- TODO
+ @p
+ This gives the correct result for most numbers, and is reasonably efficient. However, what about dividing-by-zero?
+
+ @hl.scala
+ /*JVM*/
+ 15 / 0 // ArithmeticException
+ @hl.javascript
+ /*JS*/
+ 15 / 0 // Infinity
+ (15 / 0) | 0 // 0
+
+ @p
+ On the JVM, the JVM is kind enough to throw an exception for you. However, in Javascript, the integer simply wraps around to @hl.javascript{Infinity}, which then gets truncated down to zero.
+ @p
+ So that's the current behavior of integers in Scala.js. One may ask: can we fix it? And the answer is, we can:
+ @hl.scala
+ /*JVM*/
+ 1 / 0 // ArithmeticException
+ @hl.javascript
+ /*JS*/
+ function intDivide(x, y){
+ var z = x / y
+ if (z == Infinity) throw new ArithmeticException("Divide by Zero")
+ else return z
+ }
+ intDivide(1, 0) // ArithmeticException
+ @p
+ This translation fixes the problem, and enforces that the @hl.scala{ArithmeticException} is thrown at the correct time. However, this approach causes some overhead: what was previously two primitive operations is now a function call, a local variable assignment, and a conditional. That is a lot more expensive than two primitive operations!
+
+ @sect{The Performance/Correctness Tradeoff}
+ @p
+ In the end, a lot of the semantic differences listed here come down to the same tradeoff: we could make the code behave more-like-Scala, but at a cost of adding overhead via function calls and other checks. Furthermore, the cost is paid regardless of whether the "exceptional case" is triggered or not: in the example above, every division in the program pays the cost!
+ @p
+ The decision to not support these exceptional cases comes down to a value judgement: how often do people actually depend on an exception being thrown as part of their program semantics, e.g. by catching it and performing actions? And how often are they just a way of indicating bugs? It turns out that very few @hl.scala{ArithmeticException}s, @hl.scala{ArrayIndexOutOfBoundsException}s, or similar are actually a necessary part of the program! They exist during debugging, but after that, these code paths are never relied upon "in production".
+ @p
+ Thus Scala.js goes for a compromise: in the Fast Optimization mode, we run the code with all these checks in place, so as to catch cases where these errors occur close-to-the-source and make it easy for you to debug them. In Full Optimization mode, on the other hand, we remove these checks, assuming you've already ran through these cases and found any bugs during development.
+ @p
+ This is a common pattern in situations where there's a tradeoff between debuggability and speed. In Scala.js' case, it allows us to get good debuggability in development, as well as good performance in production. There's some loss in debuggability in development, sacrificed in exchange for greater performance.
+
@sect("Why Jars instead of RequireJS/CommonJS")
@p
In JVM-land, the standard method for distributing these libraries is as @a("Maven Artifacts", href:="http://stackoverflow.com/questions/2487485/what-is-maven-artifact"). These are typically published in a public location such as @a("Maven Central", href:="http://search.maven.org/"), where others can find and download them for use. Typically downloads are done automatically by the build-tool: in Scala-JVM typically this is SBT.
diff --git a/book/src/main/scalatex/book/indepth/JavaAPIs.scalatex b/book/src/main/scalatex/book/indepth/JavaAPIs.scalatex
new file mode 100644
index 0000000..89f0a57
--- /dev/null
+++ b/book/src/main/scalatex/book/indepth/JavaAPIs.scalatex
@@ -0,0 +1,38 @@
+@p
+ Below is a list of classes from the Java Standard Library that are available from Scala.js. In general, much of @hl.scala{java.lang}, and parts of @hl.scala{java.io}, @hl.scala{java.util} and @hl.scala{java.net} have been ported over. This means that all these classes are available for use in Scala.js applications despite being part of the Java standard library.
+@p
+ There are many reasons you may want to port a Java class to Scala.js: you want to use it directly, you may be trying to port a library which uses it. In general, we haven't been porting things "for fun", and obscure classes like @hl.scala{org.omg.corba} will likely never be ported: we've been porting things as the need arises in order to support libraries (e.g. @a("Scala.Rx", href:="https://github.com/lihaoyi/scala.rx") that need them.
+
+@sect{Available Java APIs}
+
+ @ul
+ @for(data <- Book.javaAPIs)
+ @li
+ @a(data._1, href:=data._2)
+
+@sect{Porting Java APIs}
+ @p
+ The process for making Java library classes available in Scala.js is relatively straightforward:
+ @ul
+ @li
+ Find a class that you want to use in Scala.js, but is not implemented.
+ @li
+ Write a clean-room implementation in Scala, without looking at the source code of @a("OpenJDK", href:="http://openjdk.java.net/"). This is due to legal-software-license incompatibility between OpenJDK and Scala.js. Reading the docs or specification are fine, as is looking at the source of alternate implementations such as @a("Harmony", href:="http://harmony.apache.org/")
+ @li
+ Submit a pull-request to the @a("Scala.js repository", href:="https://github.com/scala-js/scala-js"), including your implementation, together with tests. See the @a("existing tests", href:="https://github.com/scala-js/scala-js/tree/master/test-suite/src/test/scala/scala/scalajs/testsuite/javalib") in the repository if you need examples of how to write your own.
+
+ @p
+ In general, this is a simple process, for "pure-Java" classes which do not use any special JVM/Java-specific APIs. However, this will not be possible for classes which do! This means that classes that make use of Java-specific things like:
+
+ @ul
+ @li
+ Threads
+ @li
+ Filesystem APIs
+ @li
+ Network APIs
+ @li
+ @hl.scala{sun.misc.Unsafe}
+
+ @p
+ And other similar APIs will either need to be rewritten to not-use them (e.g. @a("AtomicIntegers", href:="https://github.com/scala-js/scala-js/blob/master/javalib/src/main/scala/java/util/concurrent/atomic/AtomicInteger.scala") can be written without threading/unsafe APIs because Javascript is single-threaded) or can't be ported at all (e.g. @code{java.io.File}) \ No newline at end of file
diff --git a/examples/crossBuilds/simple/build.sbt b/examples/crossBuilds/simple/build.sbt
index b0ecca7..f1cda7e 100644
--- a/examples/crossBuilds/simple/build.sbt
+++ b/examples/crossBuilds/simple/build.sbt
@@ -1,8 +1,9 @@
-lazy val js = project.in(file("js")).settings(scalaJSSettings:_*).settings(
- unmanagedSourceDirectories in Compile +=
- baseDirectory.value / "shared" / "main" / "scala"
-)
-lazy val jvm = project.in(file("jvm")).settings(
+val sharedSettings = Seq(
unmanagedSourceDirectories in Compile +=
baseDirectory.value / "shared" / "main" / "scala"
-) \ No newline at end of file
+)
+
+lazy val js = project.in(file("js")).settings(scalaJSSettings:_*)
+ .settings(sharedSettings:_*)
+
+lazy val jvm = project.in(file("jvm")).settings(sharedSettings:_*) \ No newline at end of file
diff --git a/examples/crossBuilds/simple2/build.sbt b/examples/crossBuilds/simple2/build.sbt
new file mode 100644
index 0000000..a20cf1d
--- /dev/null
+++ b/examples/crossBuilds/simple2/build.sbt
@@ -0,0 +1,13 @@
+import utest.jsrunner.JsCrossBuild
+
+val cross = new JsCrossBuild(
+ // Shared settings here
+)
+
+lazy val js = cross.js.settings(
+ // JS-specific settings here
+)
+
+lazy val jvm = cross.jvm.settings(
+ // JVM-specific settings here
+)
diff --git a/examples/crossBuilds/simple2/js/shared/main/scala/simple/Simple.scala b/examples/crossBuilds/simple2/js/shared/main/scala/simple/Simple.scala
new file mode 100644
index 0000000..d3b0278
--- /dev/null
+++ b/examples/crossBuilds/simple2/js/shared/main/scala/simple/Simple.scala
@@ -0,0 +1,7 @@
+/*shared/main/scala/simple/Simple.scala*/
+package simple
+object Simple{
+ def formatTimes(timestamps: Seq[Long]): String = {
+ timestamps.map(Platform.format).mkString("\n")
+ }
+} \ No newline at end of file
diff --git a/examples/crossBuilds/simple2/js/shared/test/scala/simple/SimpleTest.scala b/examples/crossBuilds/simple2/js/shared/test/scala/simple/SimpleTest.scala
new file mode 100644
index 0000000..ec6b29f
--- /dev/null
+++ b/examples/crossBuilds/simple2/js/shared/test/scala/simple/SimpleTest.scala
@@ -0,0 +1,13 @@
+/*js/shared/test/scala/simple/SimpleTest.scala*/
+/*jvm/shared/test/scala/simple/SimpleTest.scala*/
+package simple
+import utest._
+object SimpleTest extends TestSuite{
+ val tests = TestSuite{
+ 'format{
+ 'nil - assert(Simple.formatTimes(Nil) == "")
+ 'timeZero - assert(
+ Simple.formatTimes(Seq(0)) == "December 31, 1969 4:00:00 PM PST")
+ }
+ }
+} \ No newline at end of file
diff --git a/examples/crossBuilds/simple2/js/src/main/scala/simple/Platform.scala b/examples/crossBuilds/simple2/js/src/main/scala/simple/Platform.scala
new file mode 100644
index 0000000..2a02a5e
--- /dev/null
+++ b/examples/crossBuilds/simple2/js/src/main/scala/simple/Platform.scala
@@ -0,0 +1,9 @@
+//js/src/main/scala/simple/Platform.scala
+package simple
+import scala.scalajs.js
+
+object Platform{
+ def format(ts: Long) = {
+ new js.Date(ts).toLocaleString()
+ }
+} \ No newline at end of file
diff --git a/examples/crossBuilds/simple2/jvm/shared b/examples/crossBuilds/simple2/jvm/shared
new file mode 120000
index 0000000..a12df7d
--- /dev/null
+++ b/examples/crossBuilds/simple2/jvm/shared
@@ -0,0 +1 @@
+../js/shared \ No newline at end of file
diff --git a/examples/crossBuilds/simple2/jvm/src/main/scala/simple/Platform.scala b/examples/crossBuilds/simple2/jvm/src/main/scala/simple/Platform.scala
new file mode 100644
index 0000000..1b7be56
--- /dev/null
+++ b/examples/crossBuilds/simple2/jvm/src/main/scala/simple/Platform.scala
@@ -0,0 +1,13 @@
+//jvm/src/main/scala/simple/Platform.scala
+package simple
+import java.text.SimpleDateFormat
+
+object Platform{
+ def format(ts: Long) = {
+ val fmt =
+ "MMMM d, yyyy h:mm:ss aaa z"
+ new SimpleDateFormat(fmt).format(
+ new java.util.Date(ts)
+ )
+ }
+} \ No newline at end of file
diff --git a/examples/crossBuilds/simple2/project/build.sbt b/examples/crossBuilds/simple2/project/build.sbt
new file mode 100644
index 0000000..5bd83ce
--- /dev/null
+++ b/examples/crossBuilds/simple2/project/build.sbt
@@ -0,0 +1,4 @@
+/*project/build.sbt*/
+addSbtPlugin("org.scala-lang.modules.scalajs" % "scalajs-sbt-plugin" % "0.5.5")
+
+addSbtPlugin("com.lihaoyi" % "utest-js-plugin" % "0.2.4") \ No newline at end of file
diff --git a/project/build.sbt b/project/build.sbt
index 5ac559a..3d8ec82 100644
--- a/project/build.sbt
+++ b/project/build.sbt
@@ -1 +1,3 @@
-addSbtPlugin("org.scala-lang.modules.scalajs" % "scalajs-sbt-plugin" % "0.5.5") \ No newline at end of file
+addSbtPlugin("org.scala-lang.modules.scalajs" % "scalajs-sbt-plugin" % "0.5.5")
+
+addSbtPlugin("com.lihaoyi" % "utest-js-plugin" % "0.2.4") \ No newline at end of file