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