package com.lihaoyi.workbench import akka.actor.{ActorRef, Actor, ActorSystem} import scala.concurrent.duration._ import java.nio.file.{Paths} import play.api.libs.json.Json import sbt._ import Keys._ import akka.actor.ActorDSL._ import com.typesafe.config.ConfigFactory import play.api.libs.json.JsArray import spray.http.{AllOrigins, HttpResponse} import spray.routing.SimpleRoutingApp import spray.http.HttpHeaders.`Access-Control-Allow-Origin` 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") 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 workbenchSettings = Seq( localUrl := ("localhost", 12345), extraLoggers := { val clientLogger = FullLogger{ new Logger { 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) val currentFunction = extraLoggers.value (key: ScopedKey[_]) => clientLogger +: currentFunction(key) }, refreshBrowsers := { streams.value.log.info("workbench: Reloading Pages...") pubSub ! Json.arr("reload") }, updateBrowsers := { 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) pubSub ! Json.arr( "run", "/" + cwd.relativize(filePath).toString, bootSnippet.value ) f }(Set(x)) } }, routes := startServer(localUrl.value._1, localUrl.value._2){ get{ path("workbench.js"){ complete{ IO.readStream( getClass.getClassLoader .getResourceAsStream("workbench_template.ts") ).replace("", localUrl.value._1) .replace("", localUrl.value._2.toString) .replace("", bootSnippet.value) } } ~ getFromDirectory(".") } ~ post{ path("notifications"){ ctx => pubSub ! ctx.responder } } } ) }