From 1d659fdf8f3d0670076bdc03f54309c6e48df3c2 Mon Sep 17 00:00:00 2001 From: lihaoyi Date: Sun, 17 Nov 2013 23:51:23 -0800 Subject: Added `updateBrowsers` command --- package.scala | 36 +++++++++++++++++++++++++----------- readme.md | 23 ++++++++++++++++++++--- workbench_template.ts | 30 ++++++++++++++++++++++++++++++ 3 files changed, 75 insertions(+), 14 deletions(-) diff --git a/package.scala b/package.scala index 6bc58b3..0e0e7be 100644 --- a/package.scala +++ b/package.scala @@ -22,11 +22,12 @@ import spray.can.server.websockets.model.OpCode.Text package object workbench 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") val server = settingKey[ActorRef]("local websocket server") val fileName = settingKey[String]("name of the generated javascript file") - + val bootstrapSnippet = settingKey[String]("piece of javascript to make things happen") implicit val system = ActorSystem( "SystemLol", config = ConfigFactory.load(ActorSystem.getClass.getClassLoader), @@ -46,7 +47,7 @@ package object workbench extends sbt.Plugin { localUrl := ("localhost", 12345), fileName := "workbench.js", server := { - implicit val server = system.actorOf(Props(new SocketServer)) + implicit val server = system.actorOf(Props(new SocketServer(bootstrapSnippet.value))) val host = localUrl.value io.IO(Sockets) ! Http.Bind(server, host._1, host._2) server @@ -57,8 +58,8 @@ package object workbench extends sbt.Plugin { 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", message)) - def trace(t: => Throwable): Unit = server.value.send(Json.arr("print", t.toString)) + 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)) } } val currentFunction = extraLoggers.value @@ -68,16 +69,29 @@ package object workbench extends sbt.Plugin { streams.value.log("Reloading Pages...") server.value.send(Json.arr("reload")) }, + updateBrowsers := { + println("partialReload") + server.value send Json.arr("clear") + ((crossTarget in Compile).value * "*.js").get.map{ (x: File) => + println("Checking " + x.getName) + FileFunction.cached(streams.value.cacheDirectory / x.getName, FilesInfo.lastModified, FilesInfo.lastModified){ (f: Set[File]) => + println("Refreshing " + x.getName) + server.value send Json.arr("run", f.head.getAbsolutePath, bootstrapSnippet.value) + f + }(Set(x)) + } + }, generateClient := { - val clientTemplate = IO.readStream(getClass.getClassLoader.getResourceAsStream("workbench_template.ts")) - val transformed = clientTemplate.replace("", localUrl.value._1).replace("", localUrl.value._2.toString) - val outputFile = (crossTarget in Compile).value / fileName.value - IO.write(outputFile, transformed) - outputFile + FileFunction.cached(streams.value.cacheDirectory / "workbench"/ "workbench.js", FilesInfo.full, FilesInfo.full){ (f: Set[File]) => + val transformed = IO.read(f.head).replace("", localUrl.value._1).replace("", localUrl.value._2.toString) + val outputFile = (crossTarget in Compile).value / fileName.value + IO.write(outputFile, transformed) + Set(outputFile) + }(Set(new File(getClass.getClassLoader.getResource("workbench_template.ts").getPath))).head } ) - class SocketServer extends Actor{ + class SocketServer(bootstrapSnippet: String) extends Actor{ val sockets: mutable.Set[ActorRef] = mutable.Set.empty def receive = { case x: Tcp.Connected => sender ! Tcp.Register(self) // normal Http server init @@ -97,6 +111,7 @@ package object workbench extends sbt.Plugin { case Sockets.Upgraded => sockets.add(sender) println("Browser Open n=" + sockets.size) + self send Json.arr("eval", bootstrapSnippet) case f @ Frame(fin, rsv, Text, maskingKey, data) => sockets.foreach(_ ! f.copy(maskingKey=None)) @@ -105,7 +120,6 @@ package object workbench extends sbt.Plugin { if (sockets.contains(sender)) println("Browser Closed n=" + sockets.size ) sockets.remove(sender) - case x => } } diff --git a/readme.md b/readme.md index d1675b1..762073c 100644 --- a/readme.md +++ b/readme.md @@ -16,7 +16,6 @@ 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` -- Add `refreshBrowsers <<= refreshBrowsers.triggeredBy(packageJS in Compile)` to your project settings, to make it refresh every time `packageJS` completes. - Modify the `packageJS` task with the following setting, to make it generate the snippet of `workbench.js` file needed to communicate with SBT: ```scala @@ -25,9 +24,27 @@ packageJS in Compile := { } ``` -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 run the `refreshBrowsers` command to tell it to refresh itself, and if you set up the `triggeredBy` rule as shown above, it should refresh itself automatically at the end of every `packageJS` cycle. +- Define your `bootstrapSnippet`, which is a piece of javascript to be run to start your application, e.g. `bootstrapSnippet := "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. -Current 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. +Now you have a choice of what you want to do when the code compiles: + +refreshBrowsers +=============== +`refreshBrowsers <<= refreshBrowsers.triggeredBy(packageJS in Compile)` + +This will to make any client browsers refresh every time `packageJS` completes. + +updateBrowsers +============== +`updateBrowsers <<= updateBrowsers.triggeredBy(packageJS in Compile)` + +This will attempt to perform an update without refreshing the page every time `packageJS` completes. This involves returning the state of the `body` to the initial state before any javascript was run, stripping all event listeners and clearing all repeated timeouts and intervals. This is a best-effort cleanup, and do things like clear up websocket connections or undo modifications done to `window` or `document`, but should suffice for most applications. + +------- + +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 run the `refreshBrowsers` command 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. + +Current 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. diff --git a/workbench_template.ts b/workbench_template.ts index 9577787..35376d1 100644 --- a/workbench_template.ts +++ b/workbench_template.ts @@ -1,5 +1,12 @@ var socket = (function(){ var open = false + var shadowBody = null + window.onload = function(){ + console.log("Loaded!") + shadowBody = document.body.cloneNode() + console.log("Loaded!X") + } + var start = function(){ socket = new WebSocket("ws://:/") socket.onopen = function(event){ @@ -8,10 +15,33 @@ var socket = (function(){ } socket.onmessage = function(event){ var data = JSON.parse(event.data) + if (data[0] == "reload") { console.log("Reloading page...") location.reload() } + if (data[0] == "clear"){ + document.body = shadowBody.cloneNode() + for(var i = 0; i < 99999; i++){ + clearTimeout(i) + clearInterval(i) + } + } + if (data[0] == "run"){ + var tag = document.createElement("script") + var loaded = false + tag.setAttribute("src", data[1]) + if (data[2]){ + tag.onreadystatechange = tag.onload = function() { + if (!loaded) eval(data[2]); + loaded = true; + }; + } + document.head.appendChild(tag) + } + if (data[0] == "eval"){ + eval(data[1]) + } if (data[0] == "print") console[data[1]](data[2]) } socket.onclose = function(event){ -- cgit v1.2.3