summaryrefslogblamecommitdiff
path: root/src/main/scala/workbench/Plugin.scala
blob: 4c6fb9d6b8b4e18e6fc3d55c6f89aecb28d7d05d (plain) (tree)
1
2
3
4
5
6
7
8
9
10
                             
                                                         


                 




                                                        
 
                   

                                  
                                                                                                                   

                                                                                                                               
                                                      


                                                                         
                                                                                   
                                                                                                   




                                                                                                                        
 
                              
                                     

                                   
                                                             















                                                                                                                         
                                    

                                    
                                                           


                                                                                                







                                                                 
                                            

                       


                                                                               
                                             
 


                                                                          
         

       










                                                                               


                                                                                  



                                                               


                                                                                  
           
      




































                                                                                    

                                                       



                                                                



                                                                                             


















                                                                   
                                                                         

























                                                                                         
 
package com.lihaoyi.workbench
import scala.concurrent.ExecutionContext.Implicits.global
import sbt._
import sbt.Keys._
import autowire._
import org.scalajs.sbtplugin.ScalaJSPlugin.AutoImport
import org.scalajs.core.tools.io._
import org.scalajs.core.tools.optimizer.ScalaJSOptimizer
import org.scalajs.sbtplugin.ScalaJSPluginInternal._
import org.scalajs.sbtplugin.Implicits._

import AutoImport._
object Plugin 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 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[this] 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")

  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, localUrl) map { (paths, localUrl) =>
      paths.map { path =>
        s"http://${localUrl._1}:${localUrl._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)
  ))

  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
  }
}