summaryrefslogtreecommitdiff
path: root/src/main/scala/workbench/Plugin.scala
blob: dd3cab9c59b739fc0648e6b2da12e1615ebca0c5 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
package com.lihaoyi.workbench
import scala.concurrent.ExecutionContext.Implicits.global
import sbt._
import sbt.Keys._
import autowire._
import scala.scalajs.sbtplugin.ScalaJSPlugin.ScalaJSKeys
import scala.scalajs.tools.io._
import scala.scalajs.tools.optimizer.ScalaJSOptimizer
import scala.scalajs.sbtplugin.ScalaJSPluginInternal._
import scala.scalajs.sbtplugin.Implicits._

import ScalaJSKeys._
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(
          Inputs(input = (preLinkClasspath in fastOptJS).value),
          OutputConfig(
            output = WritableFileVirtualJSFile(output),
            cache = None,
            wantSourceMap = (emitSourceMaps in fastOptJS).value,
            relativizeSourceMapBase = relSourceMapBase,
            checkIR = (checkScalaJSIR in fastOptJS).value,
            disableInliner = (inliningMode in fastOptJS).value.disabled,
            batchInline = (inliningMode in fastOptJS).value.batch),
          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, packageLauncher, 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
  }
}