summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJakob Odersky <jakob@odersky.com>2016-11-08 00:01:24 -0800
committerJakob Odersky <jakob@odersky.com>2016-11-08 16:52:27 -0800
commit137aac4781e3da76c7f1631999e70309ed09ce2f (patch)
tree2a04db27e324d8126390e53acff5d813a186a438
parent71e5666ceeab0db8bb69c3bfcd2ddef5ab982029 (diff)
downloadworkbench-137aac4781e3da76c7f1631999e70309ed09ce2f.tar.gz
workbench-137aac4781e3da76c7f1631999e70309ed09ce2f.tar.bz2
workbench-137aac4781e3da76c7f1631999e70309ed09ce2f.zip
Separate splicing and refresh into separate plugins
-rw-r--r--example/build.sbt8
-rw-r--r--src/main/scala/workbench/WorkbenchBasePlugin.scala124
-rw-r--r--src/main/scala/workbench/WorkbenchPlugin.scala166
-rw-r--r--src/main/scala/workbench/WorkbenchSplicePlugin.scala88
4 files changed, 222 insertions, 164 deletions
diff --git a/example/build.sbt b/example/build.sbt
index 65f7ed2..521ddc3 100644
--- a/example/build.sbt
+++ b/example/build.sbt
@@ -1,6 +1,11 @@
enablePlugins(ScalaJSPlugin)
+
+// dynamic page reloading
enablePlugins(WorkbenchPlugin)
+// (experimental feature) in-place code update with state preservation
+// enablePlugins(WorkbenchSplicePlugin) // disable WorkbenchPlugin when activating
+
name := "Example"
scalaVersion := "2.12.0"
@@ -10,6 +15,3 @@ version := "0.1-SNAPSHOT"
libraryDependencies ++= Seq(
"org.scala-js" %%% "scalajs-dom" % "0.9.1"
)
-
-// (experimental feature)
-spliceBrowsers <<= spliceBrowsers.triggeredBy(fastOptJS in Compile)
diff --git a/src/main/scala/workbench/WorkbenchBasePlugin.scala b/src/main/scala/workbench/WorkbenchBasePlugin.scala
new file mode 100644
index 0000000..0ec00a6
--- /dev/null
+++ b/src/main/scala/workbench/WorkbenchBasePlugin.scala
@@ -0,0 +1,124 @@
+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 WorkbenchBasePlugin extends AutoPlugin {
+
+ override def requires = ScalaJSPlugin
+
+ object autoImport {
+ 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)
+ },
+ 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
+
+}
diff --git a/src/main/scala/workbench/WorkbenchPlugin.scala b/src/main/scala/workbench/WorkbenchPlugin.scala
index 31fd659..847e052 100644
--- a/src/main/scala/workbench/WorkbenchPlugin.scala
+++ b/src/main/scala/workbench/WorkbenchPlugin.scala
@@ -10,182 +10,26 @@ import org.scalajs.sbtplugin.Implicits._
object WorkbenchPlugin extends AutoPlugin {
- override def requires = ScalaJSPlugin
+ override def requires = WorkbenchBasePlugin
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 WorkbenchBasePlugin.autoImport._
+ import WorkbenchBasePlugin.server
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)
- ))
+ refreshBrowsers <<= refreshBrowsers.triggeredBy(fastOptJS in Compile)
+ )
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/WorkbenchSplicePlugin.scala b/src/main/scala/workbench/WorkbenchSplicePlugin.scala
new file mode 100644
index 0000000..9d68ad3
--- /dev/null
+++ b/src/main/scala/workbench/WorkbenchSplicePlugin.scala
@@ -0,0 +1,88 @@
+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 WorkbenchSplicePlugin extends AutoPlugin {
+
+ override def requires = WorkbenchPlugin
+
+ object autoImport {
+ 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")
+ }
+ import autoImport._
+ import WorkbenchBasePlugin.autoImport._
+ import WorkbenchBasePlugin.server
+ import ScalaJSPlugin.AutoImport._
+
+ val spliceSettings = Seq(
+ 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()
+ }
+ }
+ },
+ spliceBrowsers <<= spliceBrowsers.triggeredBy(fastOptJS in Compile)
+ )
+
+ override def projectSettings = spliceSettings
+
+ 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
+ }
+
+}