From 71e5666ceeab0db8bb69c3bfcd2ddef5ab982029 Mon Sep 17 00:00:00 2001 From: Jakob Odersky Date: Mon, 7 Nov 2016 18:06:30 -0800 Subject: Update scalajs versions, related dependencies and remove obsolete features --- build.sbt | 48 +++-- .../src/main/scala/workbench/WorkbenchClient.scala | 36 ++-- example/build.sbt | 18 +- example/project/build.properties | 2 +- example/project/build.sbt | 3 +- example/src/main/resources/index-dev.html | 2 +- example/src/main/resources/index-opt.html | 2 +- .../src/main/scala/example/ScalaJSExample.scala | 7 +- project/Dependencies.scala | 16 ++ project/build.sbt | 2 +- shared/main/scala/workbench/Shared.scala | 15 +- src/main/scala/workbench/Plugin.scala | 204 --------------------- src/main/scala/workbench/Server.scala | 17 +- src/main/scala/workbench/WorkbenchPlugin.scala | 191 +++++++++++++++++++ 14 files changed, 269 insertions(+), 294 deletions(-) create mode 100644 project/Dependencies.scala delete mode 100644 src/main/scala/workbench/Plugin.scala create mode 100644 src/main/scala/workbench/WorkbenchPlugin.scala diff --git a/build.sbt b/build.sbt index 985fb85..237ad49 100644 --- a/build.sbt +++ b/build.sbt @@ -1,8 +1,7 @@ import sbt.Keys._ -val scalaJsVersion = "0.6.2" - val defaultSettings = Seq( + scalacOptions ++= Seq("-feature", "-deprecation"), unmanagedSourceDirectories in Compile += baseDirectory.value / "shared" / "main" / "scala", unmanagedSourceDirectories in Test += baseDirectory.value / "shared" / "test" / "scala" ) @@ -11,7 +10,7 @@ lazy val root = project.in(file(".")).settings(defaultSettings:_*).settings( name := "workbench", version := "0.3.0-SNAPSHOT", organization := "com.lihaoyi", - scalaVersion := "2.10.5", + scalaVersion := "2.10.6", sbtPlugin := true, publishArtifact in Test := false, publishTo := Some("releases" at "https://oss.sonatype.org/service/local/staging/deploy/maven2"), @@ -39,30 +38,25 @@ lazy val root = project.in(file(".")).settings(defaultSettings:_*).settings( (fullOptJS in (client, Compile)).value (artifactPath in (client, Compile, fullOptJS)).value }, - resolvers += Resolver.url("scala-js-releases", - url("http://dl.bintray.com/content/scala-js/scala-js-releases"))( - Resolver.ivyStylePatterns), - addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.1"), + addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.13"), libraryDependencies ++= Seq( - "org.scala-lang" % "scala-compiler" % scalaVersion.value, - "io.spray" % "spray-can" % "1.3.1", - "io.spray" % "spray-routing" % "1.3.1", - "com.typesafe.akka" %% "akka-actor" % "2.3.9", - "org.scala-lang.modules" %% "scala-async" % "0.9.3" % "provided", - "com.lihaoyi" %% "autowire" % "0.2.5", - "com.lihaoyi" %% "upickle" % "0.2.8" - ), - resolvers += "bintray/non" at "http://dl.bintray.com/non/maven" + Dependencies.sprayCan, + Dependencies.sprayRouting, + Dependencies.akka, + Dependencies.autowire.value, + Dependencies.upickle.value + ) ) -lazy val client = project.in(file("client")).enablePlugins(ScalaJSPlugin) - .settings(defaultSettings: _*) - .settings( - unmanagedSourceDirectories in Compile += baseDirectory.value / ".." / "shared" / "main" / "scala", - libraryDependencies ++= Seq( - "org.scala-js" %%% "scalajs-dom" % "0.8.0", - "com.lihaoyi" %%% "autowire" % "0.2.4", - "com.lihaoyi" %%% "upickle" % "0.2.6" - ), - emitSourceMaps := false -) +lazy val client = project.in(file("client")) + .enablePlugins(ScalaJSPlugin) + .settings(defaultSettings: _*) + .settings( + unmanagedSourceDirectories in Compile += baseDirectory.value / ".." / "shared" / "main" / "scala", + libraryDependencies ++= Seq( + Dependencies.autowire.value, + Dependencies.dom.value, + Dependencies.upickle.value + ), + emitSourceMaps := false + ) diff --git a/client/src/main/scala/workbench/WorkbenchClient.scala b/client/src/main/scala/workbench/WorkbenchClient.scala index dc99bbc..e98b16b 100644 --- a/client/src/main/scala/workbench/WorkbenchClient.scala +++ b/client/src/main/scala/workbench/WorkbenchClient.scala @@ -1,20 +1,22 @@ package com.lihaoyi.workbench -import upickle._ +import upickle.Js +import upickle.default +import upickle.default.{Reader, Writer} +import upickle.json import org.scalajs.dom import org.scalajs.dom.ext._ -import upickle.{Reader, Writer, Js} import scala.scalajs.js import scala.scalajs.js.annotation.JSExport -import scalajs.concurrent.JSExecutionContext.Implicits.runNow +import scalajs.concurrent.JSExecutionContext.Implicits.queue import org.scalajs.dom.raw._ /** * The connection from workbench server to the client */ -object Wire extends autowire.Server[Js.Value, upickle.Reader, upickle.Writer] with ReadWrite{ +object Wire extends autowire.Server[Js.Value, Reader, Writer] with ReadWrite{ def wire(parsed: Js.Arr): Unit = { val Js.Arr(path, args: Js.Obj) = parsed - val req = new Request(upickle.readJs[Seq[String]](path), args.value.toMap) + val req = new Request(default.readJs[Seq[String]](path), args.value.toMap) Wire.route[Api](WorkbenchClient).apply(req) } } @@ -28,7 +30,7 @@ object WorkbenchClient extends Api{ @JSExport var success = false @JSExport - def main(bootSnippet: String, host: String, port: Int): Unit = { + def main(host: String, port: Int): Unit = { def rec(): Unit = { Ajax.post(s"http://$host:$port/notifications").onComplete { case util.Success(data) => @@ -44,7 +46,7 @@ object WorkbenchClient extends Api{ if (success) println("Workbench disconnected " + e) success = false interval = math.min(interval * 2, 30000) - dom.setTimeout(() => rec(), interval) + dom.window.setTimeout(() => rec(), interval) } } @@ -59,31 +61,19 @@ object WorkbenchClient extends Api{ override def clear(): Unit = { dom.document.asInstanceOf[js.Dynamic].body = shadowBody.cloneNode(true) for(i <- 0 until 100000){ - dom.clearTimeout(i) - dom.clearInterval(i) + dom.window.clearTimeout(i) + dom.window.clearInterval(i) } } @JSExport override def reload(): Unit = { dom.console.log("Reloading page...") - dom.location.reload() + dom.window.location.reload() } @JSExport - override def run(path: String, bootSnippet: Option[String]): Unit = { + override def run(path: String): Unit = { val tag = dom.document.createElement("script").asInstanceOf[HTMLElement] - var loaded = false - tag.setAttribute("src", path) - bootSnippet.foreach{ bootSnippet => - tag.onreadystatechange = (e: dom.Event) => { - if (!loaded) { - dom.console.log("Workbench reboot") - js.eval(bootSnippet) - } - loaded = true - } - tag.asInstanceOf[js.Dynamic].onload = tag.onreadystatechange - } dom.document.head.appendChild(tag) } @JSExport diff --git a/example/build.sbt b/example/build.sbt index fed07bd..65f7ed2 100644 --- a/example/build.sbt +++ b/example/build.sbt @@ -1,25 +1,15 @@ -import com.lihaoyi.workbench.Plugin._ - -// Turn this project into a Scala.js project by importing these settings enablePlugins(ScalaJSPlugin) - -workbenchSettings +enablePlugins(WorkbenchPlugin) name := "Example" -scalaVersion := "2.11.2" +scalaVersion := "2.12.0" version := "0.1-SNAPSHOT" -resolvers += "Typesafe repository" at "http://repo.typesafe.com/typesafe/releases/" - libraryDependencies ++= Seq( - "org.scala-js" %%% "scalajs-dom" % "0.8.0" + "org.scala-js" %%% "scalajs-dom" % "0.9.1" ) -bootSnippet := "ScalaJSExample().main();" - -disableOptimizer := true - +// (experimental feature) spliceBrowsers <<= spliceBrowsers.triggeredBy(fastOptJS in Compile) - diff --git a/example/project/build.properties b/example/project/build.properties index 748703f..27e88aa 100644 --- a/example/project/build.properties +++ b/example/project/build.properties @@ -1 +1 @@ -sbt.version=0.13.7 +sbt.version=0.13.13 diff --git a/example/project/build.sbt b/example/project/build.sbt index f623fb6..2094cdd 100644 --- a/example/project/build.sbt +++ b/example/project/build.sbt @@ -1,4 +1,3 @@ - -addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.1") +addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.13") lazy val root = project.in(file(".")).dependsOn(file("../..")) diff --git a/example/src/main/resources/index-dev.html b/example/src/main/resources/index-dev.html index 364241b..6f27812 100644 --- a/example/src/main/resources/index-dev.html +++ b/example/src/main/resources/index-dev.html @@ -14,7 +14,7 @@ diff --git a/example/src/main/resources/index-opt.html b/example/src/main/resources/index-opt.html index 5abb478..d8ad3ab 100644 --- a/example/src/main/resources/index-opt.html +++ b/example/src/main/resources/index-opt.html @@ -12,7 +12,7 @@ diff --git a/example/src/main/scala/example/ScalaJSExample.scala b/example/src/main/scala/example/ScalaJSExample.scala index 0255262..3298e4f 100644 --- a/example/src/main/scala/example/ScalaJSExample.scala +++ b/example/src/main/scala/example/ScalaJSExample.scala @@ -1,6 +1,7 @@ package example import scala.scalajs.js.annotation.JSExport import org.scalajs.dom +import org.scalajs.dom.html import scala.util.Random case class Point(x: Int, y: Int){ @@ -8,14 +9,12 @@ case class Point(x: Int, y: Int){ def /(d: Int) = Point(x / d, y / d) } -// Seems like you need this for sbt ~fastOptJS to work -// mkdir ~/.sbt/0.13/plugins/target/scala-2.10/sbt-0.13/classes @JSExport object ScalaJSExample { val ctx = dom.document .getElementById("canvas") - .asInstanceOf[dom.HTMLCanvasElement] + .asInstanceOf[html.Canvas] .getContext("2d") .asInstanceOf[dom.CanvasRenderingContext2D] @@ -41,6 +40,6 @@ object ScalaJSExample { } @JSExport def main(): Unit = { - dom.setInterval(() => run, 10) + dom.window.setInterval(() => run, 10) } } diff --git a/project/Dependencies.scala b/project/Dependencies.scala new file mode 100644 index 0000000..c7e4c9e --- /dev/null +++ b/project/Dependencies.scala @@ -0,0 +1,16 @@ +import sbt._ +import org.scalajs.sbtplugin.ScalaJSPlugin.autoImport._ + +object Dependencies { + + // jvm dependencies + val sprayCan = "io.spray" % "spray-can" % "1.3.1" + val sprayRouting = "io.spray" % "spray-routing" % "1.3.1" + val akka = "com.typesafe.akka" %% "akka-actor" % "2.3.15" + + // js and shared dependencies + val autowire = Def.setting("com.lihaoyi" %%% "autowire" % "0.2.6") + val dom = Def.setting("org.scala-js" %%% "scalajs-dom" % "0.9.1") + val upickle = Def.setting("com.lihaoyi" %%% "upickle" % "0.4.3") + +} diff --git a/project/build.sbt b/project/build.sbt index 0dd657b..bfdab78 100644 --- a/project/build.sbt +++ b/project/build.sbt @@ -1 +1 @@ -addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.1") +addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.13") diff --git a/shared/main/scala/workbench/Shared.scala b/shared/main/scala/workbench/Shared.scala index f9a1434..c4c8a11 100644 --- a/shared/main/scala/workbench/Shared.scala +++ b/shared/main/scala/workbench/Shared.scala @@ -1,13 +1,15 @@ package com.lihaoyi.workbench -import upickle.{Js, Reader, Writer} +import upickle.default.{Reader, Writer} +import upickle.Js + /** * A standard way to read and write `Js.Value`s with autowire/upickle */ trait ReadWrite{ - def write[Result: Writer](r: Result) = upickle.writeJs(r) - def read[Result: Reader](p: Js.Value) = upickle.readJs[Result](p) + def write[Result: Writer](r: Result) = upickle.default.writeJs(r) + def read[Result: Reader](p: Js.Value) = upickle.default.readJs[Result](p) } /** @@ -31,8 +33,7 @@ trait Api{ def print(level: String, msg: String): Unit /** - * Execute the javascript file available at the given `path`. Optionally, - * run a `bootSnippet` after the file has been executed. + * Execute the javascript file available at the given `path`. */ - def run(path: String, bootSnippet: Option[String]): Unit -} \ No newline at end of file + def run(path: String): Unit +} diff --git a/src/main/scala/workbench/Plugin.scala b/src/main/scala/workbench/Plugin.scala deleted file mode 100644 index a120d0f..0000000 --- a/src/main/scala/workbench/Plugin.scala +++ /dev/null @@ -1,204 +0,0 @@ -package com.lihaoyi.workbench -import scala.concurrent.ExecutionContext.Implicits.global -import sbt._ -import sbt.Keys._ -import autowire._ -import org.scalajs.sbtplugin.ScalaJSPlugin -import org.scalajs.core.tools.io._ -import org.scalajs.core.tools.optimizer.ScalaJSOptimizer -import org.scalajs.sbtplugin.ScalaJSPluginInternal._ -import org.scalajs.sbtplugin.Implicits._ - -object Plugin extends AutoPlugin { - - override def requires = ScalaJSPlugin - - object autoImport { - 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 spliceBrowsers = taskKey[Unit]("Attempts to do a live update of the code running in the browser while maintaining state") - val localUrl = settingKey[(String, Int)]("localUrl") - private[Plugin] val server = settingKey[Server]("local websocket server") - - - val bootSnippet = settingKey[String]("piece of javascript to make things happen") - val updatedJS = taskKey[List[String]]("Provides the addresses of the JS files that have changed") - val sjs = inputKey[Unit]("Run a command via the sjs REPL, which compiles it to Javascript and runs it in the browser") - val replFile = taskKey[File]("The temporary file which holds the source code for the currently executing sjs REPL") - val sjsReset = taskKey[Unit]("Reset the currently executing sjs REPL") - } - import autoImport._ - import ScalaJSPlugin.AutoImport._ - - lazy val replHistory = collection.mutable.Buffer.empty[String] - - val workbenchSettings = Seq( - localUrl := ("localhost", 12345), - updatedJS := { - var files: List[String] = Nil - ((crossTarget in Compile).value * "*.js").get.foreach { - (x: File) => - streams.value.log.info("workbench: Checking " + x.getName) - FileFunction.cached(streams.value.cacheDirectory / x.getName, FilesInfo.lastModified, FilesInfo.lastModified) { - (f: Set[File]) => - val fsPath = f.head.getAbsolutePath.drop(new File("").getAbsolutePath.length) - files = fsPath :: files - f - }(Set(x)) - } - files - }, - updatedJS := { - updatedJS.value.map{ path => - val url = localUrl.value - s"http://${url._1}:${url._2}$path" - } - }, - (extraLoggers in ThisBuild) := { - val clientLogger = FullLogger{ - new Logger { - def log(level: Level.Value, message: => String) = - if(level >= Level.Info) server.value.Wire[Api].print(level.toString, message).call() - def success(message: => String) = server.value.Wire[Api].print("info", message).call() - def trace(t: => Throwable) = server.value.Wire[Api].print("error", t.toString).call() - } - } - clientLogger.setSuccessEnabled(true) - val currentFunction = extraLoggers.value - (key: ScopedKey[_]) => clientLogger +: currentFunction(key) - }, - refreshBrowsers := { - streams.value.log.info("workbench: Reloading Pages...") - server.value.Wire[Api].reload().call() - }, - updateBrowsers := { - val changed = updatedJS.value - // There is no point in clearing the browser if no js files have changed. - if (changed.length > 0) { - server.value.Wire[Api].clear().call() - - changed.foreach { path => - streams.value.log.info("workbench: Refreshing " + path) - server.value.Wire[Api].run(path, Some(bootSnippet.value)).call() - } - } - }, - spliceBrowsers := { - val changed = updatedJS.value - // There is no point in clearing the browser if no js files have changed. - if (changed.length > 0) { - for{ - path <- changed - if !path.endsWith(".js.js") - }{ - - streams.value.log.info("workbench: Splicing " + path) - val prefix = "http://localhost:12345/" - val s = munge(sbt.IO.read(new sbt.File(path.drop(prefix.length)))) - - sbt.IO.write(new sbt.File(path.drop(prefix.length) + ".js"), s.getBytes) - server.value.Wire[Api].run(path + ".js", None).call() - } - } - }, - server := new Server(localUrl.value._1, localUrl.value._2, bootSnippet.value), - (onUnload in Global) := { (onUnload in Global).value.compose{ state => - server.value.kill() - state - }} - ) ++ inConfig(Compile)(Seq( - artifactPath in sjs := crossTarget.value / "repl.js", - replFile := { - val f = sourceManaged.value / "repl.scala" - println("Creating replFile\n" + replHistory.mkString("\n")) - sbt.IO.write(f, replHistory.mkString("\n")) - f - }, - sources in Compile += replFile.value, - sjs := Def.inputTaskDyn { - import sbt.complete.Parsers._ - val str = sbt.complete.Parsers.any.*.parsed.mkString - val newSnippet = s""" - @scalajs.js.annotation.JSExport object O${replHistory.length}{ - $str - }; - import O${replHistory.length}._ - """ - replHistory.append(newSnippet) - Def.taskDyn { - // Basically C&Ped from fastOptJS, since we dont want this - // special mode from triggering updateBrowsers or similar - val s = streams.value - val output = (artifactPath in sjs).value - - val taskCache = WritableFileVirtualTextFile(s.cacheDirectory / "fastopt-js") - - sbt.IO.createDirectory(output.getParentFile) - - val relSourceMapBase = - if ((relativeSourceMaps in fastOptJS).value) - Some(output.getParentFile.toURI()) - else None - - import ScalaJSOptimizer._ - - (scalaJSOptimizer in fastOptJS).value.optimizeCP( - (scalaJSPreLinkClasspath in fastOptJS).value, - Config( - output = WritableFileVirtualJSFile(output), - cache = None, - wantSourceMap = (emitSourceMaps in fastOptJS).value, - relativizeSourceMapBase = relSourceMapBase, - checkIR = (scalaJSOptimizerOptions in fastOptJS).value.checkScalaJSIR, - disableOptimizer = (scalaJSOptimizerOptions in fastOptJS).value.disableOptimizer, - batchMode = (scalaJSOptimizerOptions in fastOptJS).value.batchMode - ), - s.log - ) - // end of C&P - val outPath = sbt.IO.relativize( - baseDirectory.value, - (artifactPath in sjs).value - ).get - - sbt.IO.write( - (artifactPath in sjs).value, - sbt.IO.read(output) + s"\n\nO${replHistory.length - 1}()" - ) - Def.task { - server.value.Wire[Api].run( - s"http://localhost:12345/$outPath", - None - ).call() - () - } - }.dependsOn(packageJSDependencies, packageScalaJSLauncher, compile) - }, - sjsReset := { - println("Clearing sjs REPL History") - replHistory.clear() - }, - sjsReset := sjsReset.triggeredBy(fastOptJS) - )) - - override def projectSettings = workbenchSettings - - def munge(s0: String) = { - var s = s0 - s = s.replace("\nvar ScalaJS = ", "\nvar ScalaJS = ScalaJS || ") - s = s.replaceAll( - "\n(ScalaJS\\.c\\.[a-zA-Z_$0-9]+\\.prototype) = (.*?\n)", - """ - |$1 = $1 || {} - |(function(){ - | var newProto = $2 - | for (var attrname in newProto) { $1[attrname] = newProto[attrname]; } - |})() - |""".stripMargin - ) - for(char <- Seq("d", "c", "h", "i", "n", "m")){ - s = s.replaceAll("\n(ScalaJS\\." + char + "\\.[a-zA-Z_$0-9]+) = ", "\n$1 = $1 || ") - } - s - } -} diff --git a/src/main/scala/workbench/Server.scala b/src/main/scala/workbench/Server.scala index 1d6d1ba..5953fcb 100644 --- a/src/main/scala/workbench/Server.scala +++ b/src/main/scala/workbench/Server.scala @@ -8,13 +8,13 @@ import spray.httpx.encoding.Gzip import spray.routing.SimpleRoutingApp import akka.actor.ActorDSL._ -import upickle.{Reader, Writer, Js} +import upickle.Js +import upickle.default.{Reader, Writer} import spray.http.{HttpEntity, AllOrigins, HttpResponse} import spray.http.HttpHeaders.`Access-Control-Allow-Origin` import concurrent.duration._ import scala.concurrent.Future import scala.io.Source -import org.scalajs.core.tools.optimizer.{ScalaJSClosureOptimizer, ScalaJSOptimizer} import org.scalajs.core.tools.io._ import org.scalajs.core.tools.logging.Level import scala.tools.nsc @@ -24,12 +24,11 @@ import scala.tools.nsc.backend.JavaPlatform import scala.tools.nsc.util.ClassPath.JavaContext import scala.collection.mutable import scala.tools.nsc.typechecker.Analyzer -import org.scalajs.core.tools.classpath.{CompleteClasspath, PartialClasspath} import scala.tools.nsc.util.{JavaClassPath, DirectoryClassPath} import spray.http.HttpHeaders._ import spray.http.HttpMethods._ -class Server(url: String, port: Int, bootSnippet: String) extends SimpleRoutingApp{ +class Server(url: String, port: Int) extends SimpleRoutingApp{ val corsHeaders: List[ModeledHeader] = List( `Access-Control-Allow-Methods`(OPTIONS, GET, POST), @@ -47,9 +46,9 @@ class Server(url: String, port: Int, bootSnippet: String) extends SimpleRoutingA /** * The connection from workbench server to the client */ - object Wire extends autowire.Client[Js.Value, upickle.Reader, upickle.Writer] with ReadWrite{ + object Wire extends autowire.Client[Js.Value, Reader, Writer] with ReadWrite{ def doCall(req: Request): Future[Js.Value] = { - longPoll ! Js.Arr(upickle.writeJs(req.path), Js.Obj(req.args.toSeq:_*)) + longPoll ! Js.Arr(upickle.default.writeJs(req.path), Js.Obj(req.args.toSeq:_*)) Future.successful(Js.Null) } } @@ -68,7 +67,7 @@ class Server(url: String, port: Int, bootSnippet: String) extends SimpleRoutingA case object Clear import system.dispatcher - system.scheduler.schedule(0 seconds, 10 seconds, self, Clear) + system.scheduler.schedule(0.seconds, 10.seconds, self, Clear) def respond(a: ActorRef, s: String) = { a ! HttpResponse( entity = s, @@ -116,7 +115,7 @@ class Server(url: String, port: Int, bootSnippet: String) extends SimpleRoutingA (function(){ $body - com.lihaoyi.workbench.WorkbenchClient().main(${upickle.write(bootSnippet)}, ${upickle.write(url)}, ${upickle.write(port)}) + com.lihaoyi.workbench.WorkbenchClient().main(${upickle.default.write(url)}, ${upickle.default.write(port)}) }).call(this) """ } @@ -132,4 +131,4 @@ class Server(url: String, port: Int, bootSnippet: String) extends SimpleRoutingA } def kill() = system.shutdown() -} \ No newline at end of file +} diff --git a/src/main/scala/workbench/WorkbenchPlugin.scala b/src/main/scala/workbench/WorkbenchPlugin.scala new file mode 100644 index 0000000..31fd659 --- /dev/null +++ b/src/main/scala/workbench/WorkbenchPlugin.scala @@ -0,0 +1,191 @@ +package com.lihaoyi.workbench +import scala.concurrent.ExecutionContext.Implicits.global +import sbt._ +import sbt.Keys._ +import autowire._ +import org.scalajs.sbtplugin.ScalaJSPlugin +import org.scalajs.core.tools.io._ +import org.scalajs.sbtplugin.ScalaJSPluginInternal._ +import org.scalajs.sbtplugin.Implicits._ + +object WorkbenchPlugin extends AutoPlugin { + + override def requires = ScalaJSPlugin + + object autoImport { + val refreshBrowsers = taskKey[Unit]("Sends a message to all connected web pages asking them to refresh the page") + val updatedJS = taskKey[List[String]]("Provides the addresses of the JS files that have changed") + val spliceBrowsers = taskKey[Unit]("Attempts to do a live update of the code running in the browser while maintaining state") + val localUrl = settingKey[(String, Int)]("localUrl") + + val sjs = inputKey[Unit]("Run a command via the sjs REPL, which compiles it to Javascript and runs it in the browser") + val replFile = taskKey[File]("The temporary file which holds the source code for the currently executing sjs REPL") + val sjsReset = taskKey[Unit]("Reset the currently executing sjs REPL") + } + import autoImport._ + import ScalaJSPlugin.AutoImport._ + + val server = settingKey[Server]("local websocket server") + + lazy val replHistory = collection.mutable.Buffer.empty[String] + + val workbenchSettings = Seq( + localUrl := ("localhost", 12345), + (extraLoggers in ThisBuild) := { + val clientLogger = FullLogger{ + new Logger { + def log(level: Level.Value, message: => String) = + if(level >= Level.Info) server.value.Wire[Api].print(level.toString, message).call() + def success(message: => String) = server.value.Wire[Api].print("info", message).call() + def trace(t: => Throwable) = server.value.Wire[Api].print("error", t.toString).call() + } + } + clientLogger.setSuccessEnabled(true) + val currentFunction = extraLoggers.value + (key: ScopedKey[_]) => clientLogger +: currentFunction(key) + }, + refreshBrowsers := { + streams.value.log.info("workbench: Reloading Pages...") + server.value.Wire[Api].reload().call() + }, + // this currently requires the old <<= syntax + // see https://github.com/sbt/sbt/issues/1444 + refreshBrowsers <<= refreshBrowsers.triggeredBy(fastOptJS in Compile), + updatedJS := { + var files: List[String] = Nil + ((crossTarget in Compile).value * "*.js").get.foreach { + (x: File) => + streams.value.log.info("workbench: Checking " + x.getName) + FileFunction.cached(streams.value.cacheDirectory / x.getName, FilesInfo.lastModified, FilesInfo.lastModified) { + (f: Set[File]) => + val fsPath = f.head.getAbsolutePath.drop(new File("").getAbsolutePath.length) + files = fsPath :: files + f + }(Set(x)) + } + files + }, + updatedJS := { + val paths = updatedJS.value + val url = localUrl.value + paths.map { path => + s"http://${url._1}:${url._2}$path" + } + }, + spliceBrowsers := { + val changed = updatedJS.value + // There is no point in clearing the browser if no js files have changed. + if (changed.length > 0) { + for{ + path <- changed + if !path.endsWith(".js.js") + }{ + streams.value.log.info("workbench: Splicing " + path) + val url = localUrl.value + val prefix = s"http://${url._1}:${url._2}/" + val s = munge(sbt.IO.read(new sbt.File(path.drop(prefix.length)))) + + sbt.IO.write(new sbt.File(path.drop(prefix.length) + ".js"), s.getBytes) + server.value.Wire[Api].run(path + ".js").call() + } + } + }, + server := new Server(localUrl.value._1, localUrl.value._2), + (onUnload in Global) := { (onUnload in Global).value.compose{ state => + server.value.kill() + state + }} + ) ++ inConfig(Compile)(Seq( + artifactPath in sjs := crossTarget.value / "repl.js", + replFile := { + val f = sourceManaged.value / "repl.scala" + sbt.IO.write(f, replHistory.mkString("\n")) + f + }, + sources in Compile += replFile.value, + sjs := Def.inputTaskDyn { + import sbt.complete.Parsers._ + val str = sbt.complete.Parsers.any.*.parsed.mkString + val newSnippet = s""" + @scalajs.js.annotation.JSExport object O${replHistory.length}{ + $str + }; + import O${replHistory.length}._ + """ + replHistory.append(newSnippet) + Def.taskDyn { + // Basically C&Ped from fastOptJS, since we dont want this + // special mode from triggering updateBrowsers or similar + val s = streams.value + val output = (artifactPath in sjs).value + + val taskCache = WritableFileVirtualTextFile(s.cacheDirectory / "fastopt-js") + + sbt.IO.createDirectory(output.getParentFile) + + val relSourceMapBase = + if ((relativeSourceMaps in fastOptJS).value) + Some(output.getParentFile.toURI()) + else None + + // TODO: re-enable this feature for latest scalajs + // NOTE: maybe use 'scalaJSOptimizerOptions in fullOptJS' + // (scalaJSOptimizer in fastOptJS).value.optimizeCP( + // (scalaJSPreLinkClasspath in fastOptJS).value, + // Config( + // output = WritableFileVirtualJSFile(output), + // cache = None, + // wantSourceMap = (emitSourceMaps in fastOptJS).value, + // relativizeSourceMapBase = relSourceMapBase, + // checkIR = (scalaJSOptimizerOptions in fastOptJS).value.checkScalaJSIR, + // disableOptimizer = (scalaJSOptimizerOptions in fastOptJS).value.disableOptimizer, + // batchMode = (scalaJSOptimizerOptions in fastOptJS).value.batchMode + // ), + // s.log + // ) + // end of C&P + val outPath = sbt.IO.relativize( + baseDirectory.value, + (artifactPath in sjs).value + ).get + + sbt.IO.write( + (artifactPath in sjs).value, + sbt.IO.read(output) + s"\n\nO${replHistory.length - 1}()" + ) + Def.task { + server.value.Wire[Api].run( + s"http://localhost:12345/$outPath" + ).call() + () + } + }.dependsOn(packageJSDependencies, packageScalaJSLauncher, compile) + }, + sjsReset := { + println("Clearing sjs REPL History") + replHistory.clear() + }, + sjsReset := sjsReset.triggeredBy(fastOptJS) + )) + + override def projectSettings = workbenchSettings + + def munge(s0: String) = { + var s = s0 + s = s.replace("\nvar ScalaJS = ", "\nvar ScalaJS = ScalaJS || ") + s = s.replaceAll( + "\n(ScalaJS\\.c\\.[a-zA-Z_$0-9]+\\.prototype) = (.*?\n)", + """ + |$1 = $1 || {} + |(function(){ + | var newProto = $2 + | for (var attrname in newProto) { $1[attrname] = newProto[attrname]; } + |})() + |""".stripMargin + ) + for(char <- Seq("d", "c", "h", "i", "n", "m")){ + s = s.replaceAll("\n(ScalaJS\\." + char + "\\.[a-zA-Z_$0-9]+) = ", "\n$1 = $1 || ") + } + s + } +} -- cgit v1.2.3