summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLi Haoyi <haoyi@dropbox.com>2014-11-09 17:10:11 -0800
committerLi Haoyi <haoyi@dropbox.com>2014-11-09 17:10:11 -0800
commit52306f10ce3d1e462b171688de04b37174c0b74a (patch)
tree82dcc4d155f9701174528f6857b9de349b2b6bc9
parent978a138c02c07822ef71f31f71e552a9659a0a53 (diff)
downloadhands-on-scala-js-52306f10ce3d1e462b171688de04b37174c0b74a.tar.gz
hands-on-scala-js-52306f10ce3d1e462b171688de04b37174c0b74a.tar.bz2
hands-on-scala-js-52306f10ce3d1e462b171688de04b37174c0b74a.zip
update
-rw-r--r--book/src/main/scalatex/book/Index.scalatex2
-rw-r--r--book/src/main/scalatex/book/handson/ClientServer.scalatex142
-rw-r--r--examples/crossBuilds/clientserver/build.sbt25
-rw-r--r--examples/crossBuilds/clientserver/js/shared/main/scala/simple/Simple.scala7
-rw-r--r--examples/crossBuilds/clientserver/js/shared/test/scala/simple/SimpleTest.scala13
-rw-r--r--examples/crossBuilds/clientserver/js/src/main/scala/simple/Platform.scala31
-rw-r--r--examples/crossBuilds/clientserver/jvm/shared/main/scala/simple/Simple.scala7
-rw-r--r--examples/crossBuilds/clientserver/jvm/shared/test/scala/simple/SimpleTest.scala13
-rw-r--r--examples/crossBuilds/clientserver/jvm/src/main/scala/simple/Platform.scala59
-rw-r--r--examples/crossBuilds/clientserver/project/build.sbt4
-rw-r--r--examples/crossBuilds/clientserver2/build.sbt28
-rw-r--r--examples/crossBuilds/clientserver2/js/shared/main/scala/simple/Simple.scala7
-rw-r--r--examples/crossBuilds/clientserver2/js/shared/test/scala/simple/SimpleTest.scala13
-rw-r--r--examples/crossBuilds/clientserver2/js/src/main/scala/simple/Platform.scala34
-rw-r--r--examples/crossBuilds/clientserver2/jvm/shared/main/scala/simple/Simple.scala7
-rw-r--r--examples/crossBuilds/clientserver2/jvm/shared/test/scala/simple/SimpleTest.scala13
-rw-r--r--examples/crossBuilds/clientserver2/jvm/src/main/scala/simple/Platform.scala61
-rw-r--r--examples/crossBuilds/clientserver2/project/build.sbt4
18 files changed, 468 insertions, 2 deletions
diff --git a/book/src/main/scalatex/book/Index.scalatex b/book/src/main/scalatex/book/Index.scalatex
index 3cb8c2a..3f0c69f 100644
--- a/book/src/main/scalatex/book/Index.scalatex
+++ b/book/src/main/scalatex/book/Index.scalatex
@@ -39,7 +39,7 @@
@sect("Publishing Modules")
@handson.PublishingModules.template
- @sect("Integrating Client and Server")
+ @sect("Integrating Client-Server")
@handson.ClientServer.template
@sect("Scala.js in Depth", "Exploring Scala.js")
diff --git a/book/src/main/scalatex/book/handson/ClientServer.scalatex b/book/src/main/scalatex/book/handson/ClientServer.scalatex
index 30404ce..b936482 100644
--- a/book/src/main/scalatex/book/handson/ClientServer.scalatex
+++ b/book/src/main/scalatex/book/handson/ClientServer.scalatex
@@ -1 +1,141 @@
-TODO \ No newline at end of file
+
+@p
+ Historically, sharing code across client & server has been a holy-grail for web development. There are many things which have made it hard in the past:
+
+@ul
+ @li
+ Javascript on the client v.s. PHP/Perl/Python/Ruby/Java on the client
+ @li
+ Most back-ends make heavy use of C extensions, and front-end code was tightly coupled to the DOM. Even if you manage to port the main language
+@p
+ There have been some attempts in recent years with more traction: Node.js, for example, has been very successful at running Javascript on the server, the Clojure/Clojurescript community has their own version of cross-built code, and there are a number of smaller, more esoteric platforms.
+
+@p
+ Scala.js lets you share code between client and server relatively straightforwardly. As we saw in the previous chapter, where we made a shared module. Let's work to turn that shared module into a working client-server application!
+
+@sect{Client-Server Configuration}
+ @p
+ Getting started with client-server integration, let's go with the simplest configuration possible: a Spray server and a Scala.js client. Most of the other web-frameworks (Play, Scalatra, etc.) will have more complex configurations, but the basic mechanism of wiring up Scala.js to your web framework will be the same.
+
+ @p
+ First, let's do the wiring in @code{build.sbt} to add Spray as a dependency,
+
+ @hl.ref("examples/crossBuilds/clientserver/build.sbt")
+
+ @p
+ This does the standard boilerplate for including dependencies via SBT, and does on additional thing: we add the output of @code{fastOptJS} from the client to the @code{resources} on the server.
+
+ @p
+ Next, let's kick off the Spray server in our Scala-JVM main method:
+
+ @hl.ref("examples/crossBuilds/clientserver/jvm/src/main/scala/simple/Platform.scala")
+
+ @p
+ This is a not-very-interesting @a("spray-routing", href:="http://spray.io/documentation/1.2.2/spray-routing/") application: we set up a server on @code{localhost:8080}, have the root URL serve a HTML string, and have other URLs serve resources. This includes the @code{js-fastopt.js} file that is now in our resources because of our @code{build.sbt} config earlier! We also add a POST route to allow the client ask the server to format dates any time it wants via Ajax.
+
+ @p
+ Lastly, we'll set up the Scala.js main method, which we are calling in the @hl.html{<script>} tag above to kick off the client-side application.
+
+ @hl.ref("examples/crossBuilds/clientserver/js/src/main/scala/simple/Platform.scala")
+
+ @p
+ Apart from the main method, we also set up an Ajax call to the POST route we defined earlier on the server, which will ask the server to do it's date formatting a second time when the page loads.
+
+ @p
+ Now, if we go to the browser at @code{localhost:8080}, we should see our web-page!
+
+ @div
+ @h1
+ Hello from Scala-JVM!
+ @p
+ December 31, 1969 4:00:00 PM PST January 13, 1970 2:15:41 AM PST
+
+ @h1
+ Hello from Scala.js!
+ @p
+ 12/31/1969, 4:00:00 PM 1/13/1970, 2:15:41 AM
+
+ @h1
+ Hello from Ajax!
+ @p
+ December 31, 1969 4:00:00 PM PST January 13, 1970 2:15:41 AM PST
+
+ @p
+ As you can see, this client-server app exercises it's shared code to render dates on both client and server!
+
+ @p
+ The app isn't a very pretty app: HTML strings everywhere, splicing strings to create the DOM, etc.. Neither is it very useful, to be able to format dates on the same way on both client and server! Nonetheless, it should give you a sense of the configuration necessary to wire up Scala.js into any existing SBT project: you simply dump the output executable from @code{fastOptJS}/@code{fullOptJS} in the web-server's resources, to let it get served to the world. Pretty neat!
+
+ @p
+ Although it's a minimal example, you can probably imagine what it would take to extend it in a traditional way: use proper client-side and server-side templating languages like @a("Twirl", href:="https://github.com/playframework/twirl") and @a("Mustache", href:="http://mustache.github.io/"). Add additional routes to our Spray server and using @hl.javascript{XMLHttpRequest} to make requests to them. Maybe add some @a("jQuery", href:="http://jquery.com/") on the client (which has a @a("Scala.js wrapper", href:="https://github.com/scala-js/scala-js-jquery") to add some interactivity. The list goes on.
+
+ @p
+ On the other hand, Scala.js doesn't just allow you to make websites the traditional way. There are many techniques which are effectively unique to the Scala.js way of doing things, and have the potential to make your program much more uniform, type-safe, and robust. We'll explore these next.
+
+@sect{Idiomatic Scala.js}
+ @p
+ The previous section showed you how to make an interactive client-server web application using Scala/Spray on the server and Scala.js on the client, but it was a pretty close-to-Javascript implementation that didn't take much advantage of the tools available in Scala.js. Let's set about remedying that!
+
+ @p
+ The main changes we will introduce in this section are:
+
+ @ul
+ @li
+ Replacing the direct use of @hl.scala{dom.XMLHttpRequest} with @hl.scala{dom.extensions.Ajax}
+ @li
+ Swapping out HTML-strings with Scalatags, to make the HTML-generation code typo-safe
+ @li
+ Using uPickle to serialize the data being transfered between client & server, to replace the ad-hoc string concatenation and splitting
+
+ @p
+ The first step we'll need to do is add Scalatags and uPickle to our @code{build.sbt} file. We want it added to both our @code{js} and @code{jvm} projects, so they'll go inside the @hl.scala{JsCrossBuild} where we put shared settings.
+
+ @hl.ref("examples/crossBuilds/clientserver2/build.sbt")
+
+ @p
+ Next, let's re-write the server to use our new tools!
+
+ @hl.ref("examples/crossBuilds/clientserver2/jvm/src/main/scala/simple/Platform.scala")
+
+ @p
+ The main changes here are:
+
+ @ul
+ @li
+ Moving the HTML template out into a separate object and using Scalatags to render it
+ @li
+ using @hl.scala{upickle.read} to deserialize the incoming Ajax call automatically, rather than having to pry it apart with @hl.scala{String.split}
+
+ @p
+ Lastly, we need to modify our client:
+
+ @hl.ref("examples/crossBuilds/clientserver2/js/src/main/scala/simple/Platform.scala")
+
+ @p
+ This involves swapping out direct use of @hl.scala{dom.XMLHttpRequest} with @hl.scala{dom.extensions.Ajax}, using Scalatags to render the fragments here, and @hl.scala{upickle.write} to deal with the serialization of the Ajax arguments.
+
+ @p
+ If we look at the output of the website, it would look exactly the same!
+
+ @div
+ @h1
+ Hello from Scala-JVM!
+ @p
+ December 31, 1969 4:00:00 PM PST January 13, 1970 2:15:41 AM PST
+
+ @h1
+ Hello from Scala.js!
+ @p
+ 12/31/1969, 4:00:00 PM 1/13/1970, 2:15:41 AM
+
+ @h1
+ Hello from Ajax!
+ @p
+ December 31, 1969 4:00:00 PM PST January 13, 1970 2:15:41 AM PST
+
+ @p
+ But we've simplified the code considerably: most notably, all the manual string-munging we did in the previous version (to build HTML, to serialize the Ajax arguments) are all gone. They've been replaced with type-safe, functional equivalents.
+
+
+@sect{Ajax calls via Autowire}
+ TODO \ No newline at end of file
diff --git a/examples/crossBuilds/clientserver/build.sbt b/examples/crossBuilds/clientserver/build.sbt
new file mode 100644
index 0000000..434fb42
--- /dev/null
+++ b/examples/crossBuilds/clientserver/build.sbt
@@ -0,0 +1,25 @@
+import utest.jsrunner.JsCrossBuild
+import scala.scalajs.sbtplugin.ScalaJSPlugin._
+import ScalaJSKeys._
+
+val cross = new JsCrossBuild(
+ // Shared settings here
+)
+
+lazy val js = cross.js.settings(
+ libraryDependencies ++= Seq(
+ "org.scala-lang.modules.scalajs" %%% "scalajs-dom" % "0.6"
+ )
+)
+
+lazy val jvm = cross.jvm.settings(
+ libraryDependencies ++= Seq(
+ "io.spray" %% "spray-can" % "1.3.2",
+ "io.spray" %% "spray-routing" % "1.3.2",
+ "com.typesafe.akka" %% "akka-actor" % "2.3.6"
+ ),
+ (resources in Compile) += {
+ (fastOptJS in (js, Compile)).value
+ (artifactPath in (js, Compile, fastOptJS)).value
+ }
+)
diff --git a/examples/crossBuilds/clientserver/js/shared/main/scala/simple/Simple.scala b/examples/crossBuilds/clientserver/js/shared/main/scala/simple/Simple.scala
new file mode 100644
index 0000000..d3b0278
--- /dev/null
+++ b/examples/crossBuilds/clientserver/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/clientserver/js/shared/test/scala/simple/SimpleTest.scala b/examples/crossBuilds/clientserver/js/shared/test/scala/simple/SimpleTest.scala
new file mode 100644
index 0000000..ec6b29f
--- /dev/null
+++ b/examples/crossBuilds/clientserver/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/clientserver/js/src/main/scala/simple/Platform.scala b/examples/crossBuilds/clientserver/js/src/main/scala/simple/Platform.scala
new file mode 100644
index 0000000..c82a112
--- /dev/null
+++ b/examples/crossBuilds/clientserver/js/src/main/scala/simple/Platform.scala
@@ -0,0 +1,31 @@
+//js/src/main/scala/simple/Platform.scala
+package simple
+
+import org.scalajs.dom.XMLHttpRequest
+
+import scala.scalajs.js
+import org.scalajs.dom
+
+import scala.scalajs.js.annotation.JSExport
+
+@JSExport
+object Platform extends{
+ def format(ts: Long) = {
+ new js.Date(ts).toLocaleString()
+ }
+ @JSExport
+ def main(container: dom.HTMLDivElement) = {
+ container.innerHTML +=
+ "<h1>Hello from Scala.js!</h1>" +
+ s"<p>${Simple.formatTimes(Seq(0, 1 << 30))}</p>"
+
+ val xhr = new XMLHttpRequest()
+ xhr.open("POST", "/formatDates")
+ xhr.onload = (e: dom.Event) => {
+ container.innerHTML +=
+ "<h1>Hello from Ajax!</h1>" +
+ s"<p>${xhr.responseText}</p>"
+ }
+ xhr.send(Seq(0, 1 << 30).mkString(","))
+ }
+} \ No newline at end of file
diff --git a/examples/crossBuilds/clientserver/jvm/shared/main/scala/simple/Simple.scala b/examples/crossBuilds/clientserver/jvm/shared/main/scala/simple/Simple.scala
new file mode 100644
index 0000000..d3b0278
--- /dev/null
+++ b/examples/crossBuilds/clientserver/jvm/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/clientserver/jvm/shared/test/scala/simple/SimpleTest.scala b/examples/crossBuilds/clientserver/jvm/shared/test/scala/simple/SimpleTest.scala
new file mode 100644
index 0000000..ec6b29f
--- /dev/null
+++ b/examples/crossBuilds/clientserver/jvm/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/clientserver/jvm/src/main/scala/simple/Platform.scala b/examples/crossBuilds/clientserver/jvm/src/main/scala/simple/Platform.scala
new file mode 100644
index 0000000..61fdc58
--- /dev/null
+++ b/examples/crossBuilds/clientserver/jvm/src/main/scala/simple/Platform.scala
@@ -0,0 +1,59 @@
+//jvm/src/main/scala/simple/Platform.scala
+package simple
+
+import java.text.SimpleDateFormat
+
+import akka.actor.ActorSystem
+import spray.http.{HttpEntity, MediaTypes}
+import spray.routing.SimpleRoutingApp
+
+object Platform extends SimpleRoutingApp{
+ def format(ts: Long) = {
+ val fmt =
+ "MMMM d, yyyy h:mm:ss aaa z"
+ new SimpleDateFormat(fmt).format(
+ new java.util.Date(ts)
+ )
+ }
+
+ def main(args: Array[String]): Unit = {
+ implicit val system = ActorSystem()
+ startServer("localhost", port = 8080){
+ get{
+ pathSingleSlash{
+ complete{
+ val msg = Simple.formatTimes(Seq(0, 1 << 30))
+ val boot =
+ "Platform().main(document.getElementById('contents'))"
+ HttpEntity(
+ MediaTypes.`text/html`,
+ s"""
+ <html>
+ <head>
+ <script src="/js-fastopt.js"></script>
+ </head>
+ <body onload="$boot">
+ <div id="contents">
+ <h1>Hello from Scala-JVM!</h1>
+ <p>$msg</p>
+ </div>
+ </body>
+ </html>
+ """
+ )
+ }
+ } ~
+ getFromResourceDirectory("")
+ } ~
+ post{
+ path("formatDates"){
+ extract(_.request.entity.asString) { e =>
+ complete {
+ Simple.formatTimes(e.split(",").map(_.toLong))
+ }
+ }
+ }
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/examples/crossBuilds/clientserver/project/build.sbt b/examples/crossBuilds/clientserver/project/build.sbt
new file mode 100644
index 0000000..5bd83ce
--- /dev/null
+++ b/examples/crossBuilds/clientserver/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/examples/crossBuilds/clientserver2/build.sbt b/examples/crossBuilds/clientserver2/build.sbt
new file mode 100644
index 0000000..7e5bddb
--- /dev/null
+++ b/examples/crossBuilds/clientserver2/build.sbt
@@ -0,0 +1,28 @@
+import utest.jsrunner.JsCrossBuild
+import scala.scalajs.sbtplugin.ScalaJSPlugin._
+import ScalaJSKeys._
+
+val cross = new JsCrossBuild(
+ libraryDependencies ++= Seq(
+ "com.scalatags" %%% "scalatags" % "0.4.2",
+ "com.lihaoyi" %%% "upickle" % "0.2.5"
+ )
+)
+
+lazy val js = cross.js.settings(
+ libraryDependencies ++= Seq(
+ "org.scala-lang.modules.scalajs" %%% "scalajs-dom" % "0.6"
+ )
+)
+
+lazy val jvm = cross.jvm.settings(
+ libraryDependencies ++= Seq(
+ "io.spray" %% "spray-can" % "1.3.2",
+ "io.spray" %% "spray-routing" % "1.3.2",
+ "com.typesafe.akka" %% "akka-actor" % "2.3.6"
+ ),
+ (resources in Compile) += {
+ (fastOptJS in (js, Compile)).value
+ (artifactPath in (js, Compile, fastOptJS)).value
+ }
+)
diff --git a/examples/crossBuilds/clientserver2/js/shared/main/scala/simple/Simple.scala b/examples/crossBuilds/clientserver2/js/shared/main/scala/simple/Simple.scala
new file mode 100644
index 0000000..d3b0278
--- /dev/null
+++ b/examples/crossBuilds/clientserver2/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/clientserver2/js/shared/test/scala/simple/SimpleTest.scala b/examples/crossBuilds/clientserver2/js/shared/test/scala/simple/SimpleTest.scala
new file mode 100644
index 0000000..ec6b29f
--- /dev/null
+++ b/examples/crossBuilds/clientserver2/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/clientserver2/js/src/main/scala/simple/Platform.scala b/examples/crossBuilds/clientserver2/js/src/main/scala/simple/Platform.scala
new file mode 100644
index 0000000..906329e
--- /dev/null
+++ b/examples/crossBuilds/clientserver2/js/src/main/scala/simple/Platform.scala
@@ -0,0 +1,34 @@
+//js/src/main/scala/simple/Platform.scala
+package simple
+
+import scala.scalajs.js
+import org.scalajs.dom
+import dom.extensions.Ajax
+import scala.scalajs.concurrent.JSExecutionContext.Implicits.runNow
+import scala.scalajs.js.annotation.JSExport
+import scalatags.JsDom.all._
+@JSExport
+object Platform extends{
+ def format(ts: Long) = {
+ new js.Date(ts).toLocaleString()
+ }
+ @JSExport
+ def main(container: dom.HTMLDivElement) = {
+ container.appendChild(
+ div(
+ h1("Hello from Scala.js!"),
+ p(Simple.formatTimes(Seq(0, 1 << 30)))
+ ).render
+ )
+ val payload = upickle.write(Seq(0L, 1L << 30))
+ Ajax.post("/formatDates", payload).foreach{ xhr =>
+ container.appendChild(
+ div(
+ h1("Hello from Ajax!"),
+ p(xhr.responseText)
+ ).render
+ )
+ }
+
+ }
+} \ No newline at end of file
diff --git a/examples/crossBuilds/clientserver2/jvm/shared/main/scala/simple/Simple.scala b/examples/crossBuilds/clientserver2/jvm/shared/main/scala/simple/Simple.scala
new file mode 100644
index 0000000..d3b0278
--- /dev/null
+++ b/examples/crossBuilds/clientserver2/jvm/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/clientserver2/jvm/shared/test/scala/simple/SimpleTest.scala b/examples/crossBuilds/clientserver2/jvm/shared/test/scala/simple/SimpleTest.scala
new file mode 100644
index 0000000..ec6b29f
--- /dev/null
+++ b/examples/crossBuilds/clientserver2/jvm/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/clientserver2/jvm/src/main/scala/simple/Platform.scala b/examples/crossBuilds/clientserver2/jvm/src/main/scala/simple/Platform.scala
new file mode 100644
index 0000000..f4ef986
--- /dev/null
+++ b/examples/crossBuilds/clientserver2/jvm/src/main/scala/simple/Platform.scala
@@ -0,0 +1,61 @@
+//jvm/src/main/scala/simple/Platform.scala
+package simple
+
+import java.text.SimpleDateFormat
+import akka.actor.ActorSystem
+import spray.http.{HttpEntity, MediaTypes}
+import spray.routing.SimpleRoutingApp
+import scalatags.Text.all._
+
+object Static{
+ val msg = Simple.formatTimes(Seq(0, 1 << 30))
+ val boot =
+ "Platform().main(document.getElementById('contents'))"
+ val page = html(
+ head(
+ script(src:="/js-fastopt.js")
+ ),
+ body(
+ onload:=boot,
+ div(id:="contents")(
+ h1("Hello from Scala-JVM!"),
+ p(msg)
+ )
+ )
+ )
+}
+object Platform extends SimpleRoutingApp{
+ def format(ts: Long) = {
+ val fmt =
+ "MMMM d, yyyy h:mm:ss aaa z"
+ new SimpleDateFormat(fmt).format(
+ new java.util.Date(ts)
+ )
+ }
+
+ def main(args: Array[String]): Unit = {
+ implicit val system = ActorSystem()
+ startServer("localhost", port = 8080){
+ get{
+ pathSingleSlash{
+ complete{
+ HttpEntity(
+ MediaTypes.`text/html`,
+ Static.page.render
+ )
+ }
+ } ~
+ getFromResourceDirectory("")
+ } ~
+ post{
+ path("formatDates"){
+ extract(_.request.entity.asString) { e =>
+ complete {
+ Simple.formatTimes(upickle.read[Seq[Long]](e))
+ }
+ }
+ }
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/examples/crossBuilds/clientserver2/project/build.sbt b/examples/crossBuilds/clientserver2/project/build.sbt
new file mode 100644
index 0000000..5bd83ce
--- /dev/null
+++ b/examples/crossBuilds/clientserver2/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