summaryrefslogtreecommitdiff
path: root/src/main/scala/workbench/WorkbenchPlugin.scala
blob: 31fd659e8faf6b5e4ed5e686b09673ff4b24baa3 (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
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
  }
}