From 137aac4781e3da76c7f1631999e70309ed09ce2f Mon Sep 17 00:00:00 2001 From: Jakob Odersky Date: Tue, 8 Nov 2016 00:01:24 -0800 Subject: Separate splicing and refresh into separate plugins --- example/build.sbt | 8 +- src/main/scala/workbench/WorkbenchBasePlugin.scala | 124 +++++++++++++++ src/main/scala/workbench/WorkbenchPlugin.scala | 166 +-------------------- .../scala/workbench/WorkbenchSplicePlugin.scala | 88 +++++++++++ 4 files changed, 222 insertions(+), 164 deletions(-) create mode 100644 src/main/scala/workbench/WorkbenchBasePlugin.scala create mode 100644 src/main/scala/workbench/WorkbenchSplicePlugin.scala 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 + } + +} -- cgit v1.2.3