summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLi Haoyi <haoyi@dropbox.com>2014-03-02 23:35:28 -0800
committerLi Haoyi <haoyi@dropbox.com>2014-03-02 23:35:28 -0800
commit9a5ecb105f6015aa50eddadafd3254b8751dc645 (patch)
tree9a362c9157c5922357ca34affc82104d1ec471d4
parent78b15120d3b146e7dc6d6822bcebc711d0c97a9d (diff)
downloadworkbench-9a5ecb105f6015aa50eddadafd3254b8751dc645.tar.gz
workbench-9a5ecb105f6015aa50eddadafd3254b8751dc645.tar.bz2
workbench-9a5ecb105f6015aa50eddadafd3254b8751dc645.zip
version 0.1 published
-rw-r--r--Plugin.scala178
-rw-r--r--build.sbt35
-rw-r--r--readme.md28
-rw-r--r--workbench_template.ts79
4 files changed, 152 insertions, 168 deletions
diff --git a/Plugin.scala b/Plugin.scala
index 1249d3a..0d18d2f 100644
--- a/Plugin.scala
+++ b/Plugin.scala
@@ -1,71 +1,72 @@
package com.lihaoyi.workbench
-import akka.actor.{Props, ActorRef, Actor, ActorSystem}
-import akka.io
-import akka.util.ByteString
-import play.api.libs.json.JsArray
-import java.nio.file.{Files, Paths}
+import akka.actor.{ActorRef, Actor, ActorSystem}
+import scala.concurrent.duration._
+import java.nio.file.{Paths}
import play.api.libs.json.Json
-import spray.can.Http
-import spray.can.server.websockets.model.Frame
-import spray.can.server.websockets.model.OpCode
-import spray.can.server.websockets.Sockets
import sbt._
import Keys._
-
+import akka.actor.ActorDSL._
import com.typesafe.config.ConfigFactory
-import scala.collection.mutable
-import akka.io.Tcp
-import spray.http._
-import spray.http.HttpHeaders.{`Access-Control-Allow-Origin`, Connection}
-import spray.can.server.websockets.model.OpCode.Text
-import spray.http.HttpRequest
import play.api.libs.json.JsArray
-import spray.http.HttpResponse
-import java.io.IOException
+import spray.http.{AllOrigins, HttpResponse}
+import spray.routing.SimpleRoutingApp
+import spray.http.HttpHeaders.`Access-Control-Allow-Origin`
-object Plugin extends sbt.Plugin {
-
- val refreshBrowsers = taskKey[Unit]("Sends a message to all connected web pages asking them to refresh the page")
- val updateBrowsers = taskKey[Unit]("partially resets some of the stuff in the browser")
- val generateClient = taskKey[File]("generates a .js file that can be embedded in your web page")
- val localUrl = settingKey[(String, Int)]("localUrl")
- private[this] val server = settingKey[ActorRef]("local websocket server")
- val fileName = settingKey[String]("name of the generated javascript file")
- val bootSnippet = settingKey[String]("piece of javascript to make things happen")
-
+object Plugin extends sbt.Plugin with SimpleRoutingApp{
implicit val system = ActorSystem(
"SystemLol",
config = ConfigFactory.load(ActorSystem.getClass.getClassLoader),
classLoader = ActorSystem.getClass.getClassLoader
)
+ val refreshBrowsers = taskKey[Unit]("Sends a message to all connected web pages asking them to refresh the page")
+ val updateBrowsers = taskKey[Unit]("partially resets some of the stuff in the browser")
+ val localUrl = settingKey[(String, Int)]("localUrl")
+ private[this] val routes = settingKey[Unit]("local websocket server")
+ val bootSnippet = settingKey[String]("piece of javascript to make things happen")
- implicit class pimpedActor(server: ActorRef){
- def send(x: JsArray) = {
- server ! Frame(
- opcode = OpCode.Text,
- data = ByteString(x.toString())
+ val pubSub = actor(new Actor{
+ var waitingActor: Option[ActorRef] = None
+ var queuedMessages = List[JsArray]()
+ case object Clear
+ import system.dispatcher
+
+ system.scheduler.schedule(0 seconds, 10 seconds, self, Clear)
+ def respond(a: ActorRef, s: String) = {
+ a ! HttpResponse(
+ entity = s,
+ headers = List(`Access-Control-Allow-Origin`(AllOrigins))
)
}
- }
+ def receive = (x: Any) => (x, waitingActor, queuedMessages) match {
+ case (a: ActorRef, _, Nil) =>
+ // Even if there's someone already waiting,
+ // a new actor waiting replaces the old one
+ waitingActor = Some(a)
+ case (a: ActorRef, None, msgs) =>
+ respond(a, "[" + msgs.mkString(",") + "]")
+ queuedMessages = Nil
+ case (msg: JsArray, None, msgs) =>
+ queuedMessages = msg :: msgs
+ case (msg: JsArray, Some(a), Nil) =>
+ respond(a, "[" + msg + "]")
+ waitingActor = None
+ case (Clear, Some(a), Nil) =>
+ respond(a, "[]")
+ waitingActor = None
+ }
+ })
- val buildSettingsX = Seq(
+ val workbenchSettings = Seq(
localUrl := ("localhost", 12345),
- fileName := "workbench.js",
- server := {
- implicit val server = system.actorOf(Props(new SocketServer()))
- val host = localUrl.value
- io.IO(Sockets) ! Http.Bind(server, host._1, host._2)
- server
- },
extraLoggers := {
val clientLogger = FullLogger{
new Logger {
- def log(level: Level.Value, message: => String): Unit =
- if(level >= Level.Info) server.value.send(Json.arr("print", level.toString(), message))
- def success(message: => String): Unit = server.value.send(Json.arr("print", "info", message))
- def trace(t: => Throwable): Unit = server.value.send(Json.arr("print", "error", t.toString))
+ def log(level: Level.Value, message: => String) =
+ if(level >= Level.Info) pubSub ! Json.arr("print", level.toString(), message)
+ def success(message: => String) = pubSub ! Json.arr("print", "info", message)
+ def trace(t: => Throwable) = pubSub ! Json.arr("print", "error", t.toString)
}
}
clientLogger.setSuccessEnabled(true)
@@ -74,18 +75,17 @@ object Plugin extends sbt.Plugin {
},
refreshBrowsers := {
streams.value.log.info("workbench: Reloading Pages...")
- server.value.send(Json.arr("reload"))
+ pubSub ! Json.arr("reload")
},
updateBrowsers := {
-
- server.value send Json.arr("clear")
+ pubSub ! Json.arr("clear")
((crossTarget in Compile).value * "*.js").get.map{ (x: File) =>
streams.value.log.info("workbench: Checking " + x.getName)
FileFunction.cached(streams.value.cacheDirectory / x.getName, FilesInfo.lastModified, FilesInfo.lastModified){ (f: Set[File]) =>
streams.value.log.info("workbench: Refreshing " + x.getName)
val cwd = Paths.get(new File("").getAbsolutePath)
val filePath = Paths.get(f.head.getAbsolutePath)
- server.value send Json.arr(
+ pubSub ! Json.arr(
"run",
"/" + cwd.relativize(filePath).toString,
bootSnippet.value
@@ -94,68 +94,26 @@ object Plugin extends sbt.Plugin {
}(Set(x))
}
},
- generateClient := {
- FileFunction.cached(streams.value.cacheDirectory / "workbench"/ "workbench.js", FilesInfo.full, FilesInfo.exists){ (f: Set[File]) =>
- val transformed =
- IO.read(f.head)
- .replace("<host>", localUrl.value._1)
- .replace("<port>", localUrl.value._2.toString)
- .replace("<bootSnippet>", bootSnippet.value)
- val outputFile = (crossTarget in Compile).value / fileName.value
- IO.write(outputFile, transformed)
- Set(outputFile)
- }(Set(new File(getClass.getClassLoader.getResource("workbench_template.ts").toURI))).head
- }
- )
-
- class SocketServer() extends Actor{
- val sockets: mutable.Set[ActorRef] = mutable.Set.empty
- def receive = {
- case x: Tcp.Connected => sender ! Tcp.Register(self) // normal Http server init
-
- case req: HttpRequest =>
- // Upgrade the connection to websockets if you think the incoming
- // request looks good
- if (req.headers.contains(Connection("Upgrade"))){
- sender ! Sockets.UpgradeServer(Sockets.acceptAllFunction(req), self)
- }else{
-
-
- try{
- val data = Files.readAllBytes(
- Paths.get(req.uri.path.toString.drop(1))
- )
- val mimeType: ContentType = req.uri.path.toString.split('.').lastOption match {
- case Some("css") => MediaTypes.`text/css`
- case Some("html") => MediaTypes.`text/html`
- case Some("js") => MediaTypes.`application/javascript`
- case _ => ContentTypes.`text/plain`
- }
- sender ! HttpResponse(
- StatusCodes.OK,
- entity=HttpEntity.apply(mimeType, data),
- headers=List(
- `Access-Control-Allow-Origin`(spray.http.AllOrigins)
- )
- )
- }catch{case _: IOException =>
- sender ! HttpResponse(StatusCodes.NotFound)
+ routes := startServer(localUrl.value._1, localUrl.value._2){
+ get{
+ path("workbench.js"){
+ complete{
+ IO.readStream(
+ getClass.getClassLoader
+ .getResourceAsStream("workbench_template.ts")
+ ).replace("<host>", localUrl.value._1)
+ .replace("<port>", localUrl.value._2.toString)
+ .replace("<bootSnippet>", bootSnippet.value)
}
+ } ~
+ getFromDirectory(".")
+ } ~
+ post{
+ path("notifications"){ ctx =>
+ pubSub ! ctx.responder
}
+ }
- case Sockets.Upgraded =>
- sockets.add(sender)
- println("Browser Open n=" + sockets.size)
- self send Json.arr("boot")
-
- case f @ Frame(fin, rsv, Text, maskingKey, data) =>
- sockets.foreach(_ ! f.copy(maskingKey=None))
-
- case _: Tcp.ConnectionClosed =>
- if (sockets.contains(sender)) println("Browser Closed n=" + sockets.size )
- sockets.remove(sender)
-
- case x =>
}
- }
+ )
}
diff --git a/build.sbt b/build.sbt
index 832ae09..dc6e303 100644
--- a/build.sbt
+++ b/build.sbt
@@ -1,15 +1,41 @@
import sbt.Keys._
-lazy val root = project.in(file(".")).dependsOn(uri("git://github.com/lihaoyi/SprayWebSockets.git"))
-
name := "workbench"
-version := "0.1-SNAPSHOT"
+version := "0.1"
-organization := "com.lihaoyi.workbench"
+organization := "com.lihaoyi"
sbtPlugin := true
+// Sonatype
+publishArtifact in Test := false
+
+publishTo <<= version { (v: String) =>
+ Some("releases" at "https://oss.sonatype.org/service/local/staging/deploy/maven2")
+}
+
+pomExtra := (
+ <url>https://github.com/lihaoyi/workbench</url>
+ <licenses>
+ <license>
+ <name>MIT license</name>
+ <url>http://www.opensource.org/licenses/mit-license.php</url>
+ </license>
+ </licenses>
+ <scm>
+ <url>git://github.com/lihaoyi/workbench.git</url>
+ <connection>scm:git://github.com/lihaoyi/workbench.git</connection>
+ </scm>
+ <developers>
+ <developer>
+ <id>lihaoyi</id>
+ <name>Li Haoyi</name>
+ <url>https://github.com/lihaoyi</url>
+ </developer>
+ </developers>
+ )
+
(resources in Compile) := {(resources in Compile).value ++ (baseDirectory.value * "*.ts").get}
resolvers += "spray repo" at "http://repo.spray.io"
@@ -18,6 +44,7 @@ resolvers += "typesafe" at "http://repo.typesafe.com/typesafe/releases/"
libraryDependencies ++= Seq(
"io.spray" % "spray-can" % "1.2.0",
+ "io.spray" % "spray-routing" % "1.2.0",
"com.typesafe.akka" %% "akka-actor" % "2.2.3",
"com.typesafe.play" %% "play-json" % "2.2.0-RC1"
)
diff --git a/readme.md b/readme.md
index 9cf6408..2749d26 100644
--- a/readme.md
+++ b/readme.md
@@ -5,32 +5,30 @@ scala-js-workbench
A SBT plugin for [scala-js](https://github.com/lampepfl/scala-js) projects to make development in the browser more pleasant.
-- Spins up a local websocket server on (by default) localhost:12345, whenever you're in the SBT console. Navigate to localhost:12345 in the browser and it'll show a simple page tell you it's alive.
-- Generates a `workbench.js` file in your packageJS output directory, which acts a stub for SBT to control the browser. You'll need to include this in your HTML page manually via a script tag.
+- Spins up a local web server on (by default) `localhost:12345`, whenever you're in the SBT console. Navigate to localhost:12345 in the browser and it'll show a simple page tell you it's alive. You can access any file within your project directory by going to `localhost:12345/path/to/file` in a browser.
- Forwards all SBT logging from your SBT console to the browser console, so you can see what's going on (e.g. when the project is recompiling) without having to flip back and forth between browser and terminal.
- Sends commands to tell the connected browsers to refresh/update every time your Scala.Js project completes a `packageJS`.
+Check out the [example app](https://github.com/lihaoyi/workbench-example-app) for a plug-and-play example of workbench in action.
+
To Use
------
- Clone this from Github into a local directory
-- Add a dependency onto the scala-js-workbench project, e.g. in `project/project/Build.sbt`
-- Add `scala.js.workbench.buildSettingsX` to your project settings in `project/Build.sbt`
-- Modify the `packageJS` task with the following setting, to make it generate the snippet of `workbench.js` file needed to communicate with SBT:
+- Add a `addSbtPlugin("com.lihaoyi" % "workbench" % "0.1")` to your `project/build.sbt`
+- Add `workbenchSettings` to your project settings in `build.sbt`:
```scala
import scala.js.workbench.Plugin._
-buildSettingsX
-
-packageJS in Compile := {
- (packageJS in Compile).value :+ scala.js.workbench.generateClient.value
-}
+workbenchSettings
```
-- Define your `bootSnippet`, which is a piece of javascript to be run to start your application, e.g. `bootSnippet := "ScalaJS.modules.example_ScalaJSExample().main();"`. scala-js-workbench requires this so it can use it to re-start your application later on its own. You do not also need to include this on the page itself, as scala-js-workbench will execute this snippet when the browser first connects.
+- Define your `bootSnippet`, which is a piece of javascript to be run to start your application, e.g. `bootSnippet := "ScalaJS.modules.example_ScalaJSExample().main();"`. scala-js-workbench requires this so it can use it to re-start your application later on its own.
+- Include a `<script src="/workbench.js"></script>` tag in your HTML page to connect the page to workbench.
+- Open the desired HTML file via it's `localhost` URL, e.g. `localhost:12345/target/scala-2.10/classes/index.html`. This should serve up the HTML file and connect it to workbench.
-Now you have a choice of what you want to do when the code compiles:
+You have a choice of what you want to do when the code compiles:
refreshBrowsers
===============
@@ -61,11 +59,7 @@ You can force the clean-up-and-reboot to happen from the browser via the shortcu
-------
-With that done, when you open a HTML page containing `workbench.js`, if you have sbt running and scala-js-workbench enabled, it should connect over websockets and start forwarding our SBT log to the browser javascript console. You can now run the `refreshBrowsers` and `updateBrowsers` commands to tell it to refresh itself, and if you set up the `triggeredBy` rule as shown above, it should refresh/update itself automatically at the end of every `packageJS` cycle.
-
-Currently still sort of flaky; in particular, it does not behave properly across `reload`s in SBT, so if the refreshes stop working you may need to `exit` and restart SBT. Also, the initial page-load/refresh while the caches are first being set up may cause things to misbehave, but refreshing the page manually once should be enough for it to stabilize.
-
-Depends on [SprayWebSockets](https://github.com/lihaoyi/SprayWebSockets) for its websocket server; this will need to be checked out into a local directory WebSockets next to your SBT project folder. See this repo (https://github.com/lihaoyi/scala-js-game-2) for a usage example.
+With this done, you should be receiving the SBT logspam (compilation, warnings, errors) in your browse console, and the page should be automatically refreshing/updating when the application gets recompiled. If you have problems setting this up, try starting from the [example app](https://github.com/lihaoyi/workbench-example-app) and working from there.
Pull requests welcome!
diff --git a/workbench_template.ts b/workbench_template.ts
index c16d02b..f9c016c 100644
--- a/workbench_template.ts
+++ b/workbench_template.ts
@@ -1,5 +1,4 @@
-var socket = (function(){
- var open = false
+(function(){
var shadowBody = null
var bootSnippet = "<bootSnippet>"
window.onload = function(){
@@ -20,45 +19,51 @@ var socket = (function(){
}
var start = function(){
- socket = new WebSocket("ws://<host>:<port>/")
- socket.onopen = function(event){
- open = true
- console.log("scala-js-workbench connected")
- }
- socket.onmessage = function(event){
- var data = JSON.parse(event.data)
+ var req = new XMLHttpRequest()
- if (data[0] == "reload") {
- console.log("Reloading page...")
- location.reload()
- }
- if (data[0] == "clear"){
- clear()
- }
- if (data[0] == "run"){
- var tag = document.createElement("script")
- var loaded = false
- console.log("Rerunning Script... " + data[1])
- tag.setAttribute("src", data[1])
- if (data[2]){
- tag.onreadystatechange = tag.onload = function() {
- if (!loaded) eval(data[2]);
- loaded = true;
- };
+ req.open("POST", "http://<host>:<port>/notifications")
+
+ req.onload = function(){
+ if (req.status != 200){
+ setTimeout(function(){start()}, 1000)
+ }else{
+ var dataList = JSON.parse(req.responseText)
+ for(var i = 0; i < dataList.length; i++){
+
+ var data = dataList[i]
+ if (data[0] == "reload") {
+ console.log("Reloading page...")
+ location.reload()
+ }
+ if (data[0] == "clear"){
+ clear()
+ }
+ if (data[0] == "run"){
+ var tag = document.createElement("script")
+ var loaded = false
+ tag.setAttribute("src", "http://<host>:<port>" + data[1])
+ var bootSnippet = data[2]
+ if (bootSnippet){
+ tag.onreadystatechange = tag.onload = function() {
+ console.log("Post-run reboot")
+ if (!loaded) {
+ console.log("Post-run reboot go!")
+ eval(bootSnippet)
+ }
+ loaded = true
+ };
+ }
+ document.head.appendChild(tag)
+ }
+ if (data[0] == "boot"){
+ eval(bootSnippet)
+ }
+ if (data[0] == "print") console[data[1]](data[2])
}
- document.head.appendChild(tag)
- }
- if (data[0] == "boot"){
- eval(bootSnippet)
+ start()
}
- if (data[0] == "print") console[data[1]](data[2])
- }
- socket.onclose = function(event){
- if (open) console.log("scala-js-workbench disconnected")
- open = false
- setTimeout(function(){start()}, 1000)
}
+ req.send()
}
start()
- return socket
})()